11

I'm on branch a. I want to merge branch b into branch c. The merge is not a fast-forward, but it also doesn't require manual resolution. (i.e., it's not the simplest case, but it's also not the most difficult one, so it's a merge that Git is able to do on its own without needing a human.)

Is there a way for me to do this merge from b to c without having to check out any branch? How?

UPDATE: If you know of an alternative Git implementation that can do it, that would be a valid solution as well. But writing a script that would do the checkouts programmatically would not be a good solution, because it would still require me to have a clean working directory.

Ram Rachum
  • 84,019
  • 84
  • 236
  • 374

5 Answers5

8

If the merge doesn't involve files touched on both branches then I think you want git read-tree and git write-tree with a sideband GIT_INDEX_FILE. This should do it:

#!/bin/sh
export GIT_INDEX_FILE=.git/aux-merge-index
trap 'rm -f '"'$GIT_INDEX_FILE'" 0 1 2 3 15
set -e
git read-tree -im `git merge-base $2 $1` $2 $1
git write-tree \
| xargs -i@ git commit-tree @ -p $2 -p $1 -m "Merge $1 into $2" \
| xargs git update-ref -m"Merge $1 into $2" refs/heads/$2

You could also use git mktree </dev/null instead of the merge-base to treat b and c as entirely unrelated branches and make the resulting merge combine the files in each rather than treating files missing in either as deletions.

You say your merge isn't a fast-forward, so you're going to need to read the read-tree docs to make the sequence above do exactly what you want. --aggressive looks like it might be right, it depends on what the actual differences between your branches are.

edit added the empty-tree base to handle unrelated trees edit 2 hoisting some payload I said or left implicit in a comment

jthill
  • 55,082
  • 5
  • 77
  • 137
  • 1
    Okay, my mind is successfully blown. Care to explain what you did here? – Ram Rachum May 22 '12 at 17:52
  • 1
    The index is pretty much just thumbs into the object db combined with timestamps for files in your work tree. read-tree loads the index from the specified tree(s). -i says ignore the worktree, -m says do simple merges. write-tree puts the index state into the repo, commit-tree makes a commit for that, update-ref updates the ref. You'll have to read the docs on read-tree's merge options, what's here is set up not to delete files under any circumstance, which may not be what you want. If you want deletions on either branch to propagate take the `git mktree – jthill May 22 '12 at 18:34
  • Okay, I finally had time to try it. I just had to fix `I@` into `i@` and it worked! (Typo?) If this can work reliably, it's definitely better than the auxiliary-repo approach. Now a couple of questions: 1. I want it to use a completely standard merge strategy. Does it? 2. Can we make it create a standard commit message for the merge? – Ram Rachum May 25 '12 at 09:27
  • Also, I don't understand how you can use the empty-tree base in a 3-way merge. Don't you need a common parent of the 2 commits? – Ram Rachum May 25 '12 at 10:12
  • Also, if/when you do `exit 1`, aren't you left with a dirty `GIT_INDEX_FILE` setting? – Ram Rachum May 25 '12 at 10:26
  • I tried this script now (with correct branch names of course) in a real-life scenario and it failed, despite the merge being a trivial fast-forward. It failed with `my_project/templates/registration/my_template.html: unmerged (a0bc9afd6f099abb370bb5d41325bb77d3729fb3)` and then a bunch of more error messages. Any idea why? – Ram Rachum May 25 '12 at 11:44
  • re `I@`, not a typo. What OS/distro are you on? ... re the base, the merge needs three commits. There's no reason to require any particular chain of parent links between any of them. I think it's probably not what you want here, though, and I shouldn't have left some of what I said in the comment buried there. I'll edit it now. – jthill May 25 '12 at 13:33
  • @RamRachum okay, I think the answer will be more directly helpful now. You _do_ still need to do some research and adapt this to your situation, the lowlevel tools can make your life very easy because they're direct and powerful. also, re the exit code, yah, you could write a trap and put it in a script but as the unset at the end implies this kind of thing you can just type in. People don't appreciate how hard it is to actually damage a git repo. The worst you can do is leave experimental commits lying around, and enabling experimental commits is for my money the entire point of a dvcs. – jthill May 25 '12 at 14:14
  • I edited the script to be more general. If I did a mistake fill free to correct, I'm very new to Bash. – Ram Rachum May 25 '12 at 17:29
  • np, it seems people generally avoid using bash for scripts unless there's a real need for one of its features, it's been easy enough for me to live within that and it is more portable when others prefer a different shell. I reworked the error handling to be more reliable and simpler. It takes a lot to get beyond what `sh` can do. At the risk of breaking something, let me say again I see my answer as an example not a self-contained thing. – jthill May 25 '12 at 17:51
  • What situations do you think this answer is problematic for? – Ram Rachum May 25 '12 at 18:59
  • Sorry if this is a bit cryptic, but use it. Different ways. See what it does. That will be a better answer than any I could give. – jthill May 25 '12 at 19:03
5

Given your requirement to not have to clean your working directory, I assume you mean that you don't want to have to clean either your working tree or index, even via some scripting. In that case, you won't find a solution within the bounds of your current local repo. Git uses the index extensively when merging. I'm not sure about the working tree if there are no conflicts, but in general, merging is inextricably tied to the currently-checked-out branch.

There's another way, though, that wouldn't require you to change anything in your current repo. It does, however, require you to have or create a clone of your repo. Basically, just clone your repo, then do your merge in the clone, and push it back to your original repo. Here's a brief example of how it would work.

First, we need a sample repo to work with. The following sequence of commands will create one. You'll end up with master as your current branch and two other branches with changes ready to be merged named change-foo and change-bar.

mkdir background-merge-example
cd background-merge-example
git init
echo 'from master' > foo
echo 'from master' > bar
git add .
git commit -m "add foo and bar in master" 
git checkout -b change-foo
echo 'from foo branch' >> foo
git commit -am "update foo in foo branch"
git checkout -b change-bar master
echo 'from bar branch' >> bar
git commit -am "update bar in bar branch"
git checkout master

Now, imagine that you're working on master, and you're wanting to merge change-bar into change-foo. Here's a semi-graphical depiction of where we are:

$ git log --oneline --graph --all
* c60fd41 update bar in bar branch
| * e007aff update foo in foo branch
|/  
* 77484e1 add foo and bar in master

The following sequence will accomplish the merge without interfering with the current master branch. Pack this into a script, and you've got a nice "background-merge" command:

# clone with absolute instead of relative path, or the remote in the clone will
# be wrong
git clone file://`realpath .` tmp
cd tmp
# this checkout auto-creates a remote-tracking branch in newer versions of git
# older versions will have to do it manually
git checkout change-foo
# creating a tracking branch for the other remote branch is optional
# it just makes the commit message look nicer
git branch --track change-bar origin/change-bar
git merge change-bar
git push origin change-foo
cd ..
rm -rf tmp

Briefly, that will clone the current repo to a subdirectory, enter that directory, do the merge, then push it back to the original repo. It removes the subdirectory after it's done. In a large project, you might want to have a dedicated clone that's just kept up to date instead of making a fresh clone every time. After the merge and push, we end up at:

$ git log --oneline --graph --all
*   24f1916 Merge branch 'change-bar' into change-foo
|\  
| * d7375ac update bar in bar branch
* | fed4757 update foo in foo branch
|/  
* 6880cd8 add foo and bar in master

Questions?

Ryan Stewart
  • 126,015
  • 21
  • 180
  • 199
  • Thanks for the extensive write-up. I'm still digesting this. Couple of questions in the meantime: 1. I remember reading about there being 3 types of git clone: Standard, Full-Copy, and Shared. (Terminology may be idiosyncratic to git-gui.) I've only used the Full Copy one. Should I use any of the other ones for this? 2. I heard that Git also allows "bare" repos, where there is no working directory. Does Git allow merges at all on these kind of repos? If so, would that be a good choice for the temporary repository? – Ram Rachum May 19 '12 at 20:32
  • 2
    3 types: shallow, bare, "normal". Shallow copy of a git repository would not be advised in this case (using --depth option when cloning). I would not certify that you will be able to perform merges of 2 branches with a shallow copy, in particular if you miss the common parent of the 2 branches. But yes, git allows merges on shallow clones (not bare repositories) since you can use `git pull`. In this case, I would do a full/simple clone. – Vincent B. May 19 '12 at 21:12
  • About your statement above, "Git uses the index extensively when merging": Do you think that there's a way to create an auxiliary index file in the same repo that Git will do the merge in, instead of creating another repo? – Ram Rachum May 20 '12 at 08:54
  • I'd say not, but that's only an educated guess. I suppose it's possible that you could get away with some creative use of symlinks, but then it's also possible you could completely destroy your repository. What you're asking for makes me think you'd have to look into the Git source code to get a real answer, and that's a line I've never yet crossed. Sorry I can't help any more than that. Given my own experience and the lack of other answers here, my ultimate advice is to change how you're doing things so that you don't need to figure out how to do this. It sounds like trouble. – Ryan Stewart May 20 '12 at 20:52
3

Additional to the very sophisticated git read-tree answer by @jthill, there is (nowadays) an IMHO easier way doing this utilising git worktree. This is the basic approach:

$ git worktree add /tmp/wt c
$ git -C /tmp/wt merge b
$ git worktree remove /tmp/wt

Below is a little Bash script which you can call like this:

$ worktree-merge c b

Script worktree-merge:

#!/usr/bin/env bash

log() {
    echo -n "LOG: "
    echo "$@" >&2
}

escape() {
    local string="$1"
    echo "${string//[. \/]/-}"
}

cleanup() {
    trap "" SIGINT
    log "Removing temporary worktree '$1' ..."
    git worktree remove --force "$1"
}

prepare_worktree() {
    local reference="$1"
    local worktree="/tmp/MERGE-INTO-`escape "$reference"`"

    log "Creating temporary worktree '$worktree' ..."
    trap "cleanup $worktree" EXIT
    git worktree add --force "$worktree" "$reference"
}

do_merge() {
    local reference="$1"
    local worktree="/tmp/MERGE-INTO-`escape "$reference"`"
    shift

    log "Merging ${@@Q} into ${reference@Q} ..."
    git -C "$worktree" merge "$@"
}

prepare_worktree "$1" &&
do_merge "$@" &&
true
doak
  • 809
  • 9
  • 24
1

You can write a script even if your working directory is dirty. You will have to stash your changes first.

git stash
git checkout c
git merge b
git checkout a
git stash pop
Jacob Groundwater
  • 6,581
  • 1
  • 28
  • 42
  • 1
    I would also suggest using `git stash`, and that's what I use frequently to do almost the same thing as the OP does. – neevek May 19 '12 at 17:31
  • Stashing is slow on Windows (which I work on), so having to wait so long would be unattractive to me. – Ram Rachum May 19 '12 at 17:31
  • Then I don't think there're alternatives, anyhow the branch onto which you want to merge must be the *current branch*. Without stashing or committing, you can't leave your current branch `a`...Or some git experts out there have better solutions? – neevek May 19 '12 at 17:40
  • Unfortunately @RamRachum I don't think there's gonna be a perfect solution for you. You could try Mercurial, I've heard it plays well with Windows. – Jacob Groundwater May 19 '12 at 19:09
  • I tried it out today when I heard it was coming out but I don't think it solves my problem. Regardless of my problem, it does not have keyboard shortcuts, and that's a dealbreaker for me. – Ram Rachum May 21 '12 at 17:41
-1

This answer explains a workaround that you can try.

No, there is not. A checkout of the target branch is necessary to allow you to resolve conflicts, among other things (if Git is unable to automatically merge them).

However, if the merge is one that would be fast-forward, you don't need to check out the target branch, because you don't actually need to merge anything - all you have to do is update the branch to point to the new head ref. You can do this with git branch -f:

git branch -f branch-b branch-a
Will update branch-b to point to the head of branch-a.

Community
  • 1
  • 1
sparrow
  • 1,888
  • 11
  • 22