Merge without changing the working directory

I have the following script:

* ab82147 (HEAD, topic) changes * 8993636 changes * 82f4426 changes * 18be5a3 (master) first 

I would like to combine the (non fast-forward) topic into master . This requires me to:

  • git checkout master
  • git merge --no-ff topic

But, checking the wizard, and then merging the topic with it, causes git to change my working directory (although the end result is identical to the first before checking the wizard), and the problem I'm connected with is related to the size of our project, it takes about 30 minutes to build him (with IncrediBuild), although nothing has changed, and it is simply unbearable.

So, I would like to get the following:

 * 9075cf4 (HEAD, master) Merge branch 'topic' |\ | * ab82147 (topic) changes | * 8993636 changes | * 82f4426 changes |/ * 18be5a3 first 

Without touching the working directory (or at least tricking git somehow).

+14
git
Aug 04 '10 at 18:22
source share
4 answers

Interesting! I don’t think there is a built-in way to do this, but you should be able to use it with plumbing:

 #!/bin/bash branch=master # or take an argument: # if [ $@ eq 1 ]; # branch="$1"; # fi # make sure the branch exists if ! git rev-parse --verify --quiet --heads "$branch" > /dev/null; then echo "error: branch $branch does not exist" exit 1 fi # make sure this could be a fast-forward if [ "$(git merge-base HEAD $branch)" == "$(git rev-parse $branch)" ]; then # find the branch name associated with HEAD currentbranch=$(git symbolic-ref HEAD | sed 's@.*/@@') # make the commit newcommit=$(echo "Merge branch '$currentbranch'" | git commit-tree $(git log -n 1 --pretty=%T HEAD) -p $branch -p HEAD) # move the branch to point to the new commit git update-ref -m "merge $currentbranch: Merge made by simulated no-ff" "refs/heads/$branch" $newcommit else echo "error: merging $currentbranch into $branch would not be a fast-forward" exit 1 fi 

An interesting bit is the string newcommit= ; it uses commit-tree to directly create a merge commit. The first argument is the tree to use; that the tree is HEAD, the branch whose contents you want to keep. The commit message is provided on stdin, and the rest of the arguments are given to parents, who should have a new commit. The fixed SHA1 prints to stdout, so assuming the commit is successful, you can capture it and then merge it commit (this will be fast forward). If you are obsessive, you can make sure the commit-tree succeeded - but that should be pretty much guaranteed.

Limitations:

  • This only works on mergers that could be fast. Obviously, you really will need to check and merge (perhaps in a clone to save your build system) in this case.
  • The reflog message is different. I did this consciously because when you use --no-ff , git will actually force itself to use the default (recursive) strategy, but to write what would be a lie in the reflog.
  • If you are in HEAD standby mode, everything will be bad. This should be considered on purpose.

And yes, I tested this on a repo game, and it seems to work fine! (Although I did not try to break it.)

+8
Aug 04 '10 at 20:05
source share

The easiest way I can imagine is to git clone into a separate working copy, merge there, then git pull back. Then the traction will be fast forward and should only affect files that have really changed.

Of course, with such a large project, temporary clones are not perfect and need enough free space on the hard drive. The time cost of an additional clone can be minimized (in the long run) by saving your copy of the merge if you do not need disk space.

Disclaimer: I have not confirmed that this works. I believe it should, though (git doesn't support version file timestamps)

+3
Aug 04 '10 at 20:15
source share

In addition, you can correct symptoms directly by saving and restoring file timestamps. This is pretty ugly, but it was interesting to write.

Script Save / Restore Time Icon

 #!/usr/bin/env python from optparse import OptionParser import os import subprocess import cPickle as pickle try: check_output = subprocess.check_output except AttributeError: # check_output was added in Python 2.7, so it not always available def check_output(*args, **kwargs): kwargs['stdout'] = subprocess.PIPE proc = subprocess.Popen(*args, **kwargs) output = proc.stdout.read() retcode = proc.wait() if retcode != 0: cmd = kwargs.get('args') if cmd is None: cmd = args[0] err = subprocess.CalledProcessError(retcode, cmd) err.output = output raise err else: return output def git_cmd(*args): return check_output(['git'] + list(args), stderr=subprocess.STDOUT) def walk_git_tree(rev): """ Generates (sha1,path) pairs for all blobs (files) listed by git ls-tree. """ tree = git_cmd('ls-tree', '-r', '-z', rev).rstrip('\0') for entry in tree.split('\0'): print entry mode, type, sha1, path = entry.split() if type == 'blob': yield (sha1, path) else: print 'WARNING: Tree contains a non-blob.' def collect_timestamps(rev): timestamps = {} for sha1, path in walk_git_tree(rev): s = os.lstat(path) timestamps[path] = (sha1, s.st_mtime, s.st_atime) print sha1, s.st_mtime, s.st_atime, path return timestamps def restore_timestamps(timestamps): for path, v in timestamps.items(): if os.path.isfile(path): sha1, mtime, atime = v new_sha1 = git_cmd('hash-object', '--', path).strip() if sha1 == new_sha1: print 'Restoring', path os.utime(path, (atime, mtime)) else: print path, 'has changed (not restoring)' elif os.path.exists(path): print 'WARNING: File is no longer a file...' def main(): oparse = OptionParser() oparse.add_option('--save', action='store_const', const='save', dest='action', help='Save the timestamps of all git tracked files') oparse.add_option('--restore', action='store_const', const='restore', dest='action', help='Restore the timestamps of git tracked files whose sha1 hashes have not changed') oparse.add_option('--db', action='store', dest='database', help='Specify the path to the data file to restore/save from/to') opts, args = oparse.parse_args() if opts.action is None: oparse.error('an action (--save or --restore) must be specified') if opts.database is None: repo = git_cmd('rev-parse', '--git-dir').strip() dbpath = os.path.join(repo, 'TIMESTAMPS') print 'Using default database:', dbpath else: dbpath = opts.database rev = git_cmd('rev-parse', 'HEAD').strip() print 'Working against rev', rev if opts.action == 'save': timestamps = collect_timestamps(rev) data = (rev, timestamps) pickle.dump(data, open(dbpath, 'wb')) elif opts.action == 'restore': rev, timestamps = pickle.load(open(dbpath, 'rb')) restore_timestamps(timestamps) if __name__ == '__main__': main() 

Bash Test Script

 #!/bin/bash if [ -d working ]; then echo "Cowardly refusing to mangle an existing 'working' dir." exit 1 fi mkdir working cd working # create the repository/working copy git init # add a couple of files echo "File added in master:r1." > file-1 echo "File added in master:r1." > file-2 mkdir dir echo "File added in master:r1." > dir/file-3 git add file-1 file-2 dir/file-3 git commit -m "r1: add-1, add-2, add-3" git tag r1 # sleep to ensure new or changed files won't have the same timestamp echo "Listing at r1" ls --full-time sleep 5 # make a change echo "File changed in master:r2." > file-2 echo "File changed in master:r2." > dir/file-3 echo "File added in master:r2." > file-4 git add file-2 dir/file-3 file-4 git commit -m "r2: change-2, change-3, add-4" git tag r2 # sleep to ensure new or changed files won't have the same timestamp echo "Listing at r2" ls --full-time sleep 5 # create a topic branch from r1 and make some changes git checkout -b topic r1 echo "File changed in topic:r3." > file-2 echo "File changed in topic:r3." > dir/file-3 echo "File added in topic:r3." > file-5 git add file-2 dir/file-3 file-5 git commit -m "r3: change-2, change-3, add-5" git tag r3 # sleep to ensure new or changed files won't have the same timestamp echo "Listing at r3" ls --full-time sleep 5 echo "Saving timestamps" ../save-timestamps.py --save echo "Checking out master and merging" # merge branch 'topic' git checkout master git merge topic echo "File changed in topic:r3." > file-2 # restore file-2 echo "File merged in master:r4." > dir/file-3 git add file-2 dir/file-3 git commit -m "r4: Merge branch 'topic'" git tag r4 echo "Listing at r4" ls --full-time echo "Restoring timestamps" ../save-timestamps.py --restore ls --full-time 

I will leave this as a reading exercise to clear the Python script to remove extraneous output and add better error checking.

0
Aug 05 '10 at 19:47
source share

Here is some kind of choppy version.

  • git stash
  • git tag tmptag
  • git merge --no-ff topic
  • git checkout tmptag (-b tha_brunch)?
  • git stash pop
  • git tag -D tmptag
0
Aug 05 '10 at 20:12
source share



All Articles