Merging without changing the working directory

The simplest way I can think of would be to git clone to a separate working copy, do the merge there, then git pull back. The pull will then be a fast forward and should only affect files which really have changed.

Of course, with such a large project making temporary clones isn't ideal, and needs a fair chunk of extra hard disk space. The time cost of the extra clone can be minimised (in the long term) by keeping your merging-copy around, as long as you don't need the disk space.

Disclaimer: I haven't verified that this works. I believe it should though (git doesn't version file timestamps)


Interesting! I don't think there's a built-in way to do this, but you should be able to fudge it using the 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

The interesting bit is that newcommit= line; it uses commit-tree to directly create the merge commit. The first argument is the tree to use; that's the tree HEAD, the branch whose contents you want to keep. The commit message is supplied on stdin, and the rest of the arguments name the parents the new commit should have. The commit's SHA1 is printed to stdout, so assuming the commit succeeded, you capture that, then merge that commit (that'll be a fast-forward). If you're obsessive, you could make sure that commit-tree succeeded - but that should be pretty much guaranteed.

Limitations:

  • This only works on merges that could have been a fast-forward. Obviously you'll actually have to check out and merge (possibly in a clone, to save your build system) in that case.
  • The reflog message is different. I did this deliberately, because when you use --no-ff, git will actually force itself to use the default (recursive) strategy, but to write that in the reflog would be a lie.
  • If you're in detached HEAD mode, things will go badly. That would have to be treated specially.

And yes, I tested this on a toy repo, and it appears to work properly! (Though I didn't try hard to break it.)


It is absolutely possible to do any merge, even non-fast forward merges, without git checkout, messing with the commit history, or clones. The secret is to add a second "worktree", so you effectively have a primary and secondary checkouts within the same repo.

cd local_repo
git worktree add _master_wt master
cd _master_wt
git pull origin master:master
git merge --no-ff -m "merging workbranch" my_work_branch
cd ..
git worktree remove _master_wt

You have now merged the local work branch to the local master branch without switching your checkout.

Tags:

Git