Question

Given a repo with two branches, each with independent commits:

Branch  Commits
------  -----------

final:        e-g-i
             /     \
master: a-b-c-d-f-h-?

The letters in the chart above are significant: ie, "master" and "final" were under development simultaneously, and commits in both branches must be retained.

Is it better (safer) to merge "master" into "final", then merge that back into "master"? Or will merging directly into "master" from "final" retain the commits there previously?

I understand that merge conflicts will likely arise. I'm much more concerned that we don't lose any of the code committed to "master" after merging "final" into it.

Was it helpful?

Solution

Is it better (safer) to merge "master" into "final", then merge that back into "master"? Or will merging directly into "master" from "final" retain the commits there previously?

The latter: merging directly into "master" from "final" retain(s) the commits there previously.

git is different than other version control systems like SVN. For the unfamiliar, SVN treats branches like "buckets" - the branches/buckets are fixed (stable) and you had to be careful about moving commits between buckets. In SVN, you had to merge (copy) any recent 'master' commits into 'final' before you 'reintegrate' (pseudo-merge) the 'final' branch back into 'master'. 'Reintegration' effectively copies the tip of 'final' onto the tip of 'master', effectively replacing 'master' with an exact copy of the tip of 'final'. You could lose any commits in 'master' that weren't merged (copied) over to 'final' first.

Like I said, git is different. In git, I like to think of the repository tree (the commits) as stable and rather its the "branches" that move - think of branches as little labels/decorations that hang off of commits.

I'm going to draw your tree a little differently - in fact, I will draw it before commit 'i' was created:

              final
                |
            e - g
          /
a - b - c 
          \
            d - f - h
                    |
                  master

Now let's commit 'i':

                  final
                    |
            e - g - i
          /
a - b - c 
          \
            d - f - h
                    |
                  master

Only 2 things changed. (1) Ignoring the branch "decorations" for the moment, we see new commit 'i' was created from its parent commit 'g', and (2) the final "decoration" moved from commit 'g' to 'i'.

Let's merge final into master - that is, let's update master so that it includes the changes from final:

                  final
                    |
            e - g - i
          /           \
a - b - c              j
          \           /|
            d - f - h  |
                       |
                     master

Now the 'master' branch decoration moved. The 'final' branch decoration stayed put. The 'j' commit is a merge commit created from 2 parents: 'h' and 'i'.

But what if we had merged master into final instead - that is, updated final so that it includes the changes from master:

                     final
                       |
            e - g - i  |
          /           \|
a - b - c              j
          \           /
            d - f - h
                    |
                  master

Now 'final' moved and 'master' stayed in place. Note that the merge is a true merge - the changes from both branches are in commit 'j'. 'j' is the same no matter which branch merged into which - the only difference is which branch "decoration" got moved. (And those branch "decorations" are easily moved around.)

For completeness, let's merge the other branch back into the first branch (doesn't matter who merged into who first - as long as we merge in the other direction this time). Note that only the decoration moves - no new commit was necessary (in git jargon, it was 'fast-forwarded') because 'j' already contained both sets of changes from both branches:

                     final
                       |
            e - g - i  |
          /           \|
a - b - c              j
          \           /|
            d - f - h  |
                       |
                     master

In fact, let's look at the tree a few days later (I deleted 'final' branch - I was done with it):

            e - g - i
          /           \
a - b - c              j - k - l - m
          \           /            |
            d - f - h              |
                                   |
                                 master

Okay, can you tell from the tree which branch was originally 'master' vs which was originally 'final': e-g-i or d-f-h? Does it matter?

In git, merge are true merges. There are no "buckets" and you don't have to move commits between "buckets"/branches like you do for SVN. There's only the "tree" that you are evolving and the branches ("decorations") are bookmarking your place(s) in it.

OTHER TIPS

There's no difference. You can see the changes git will be merging by doing

git diff master...final

and

git diff final...master

each of which will show the accumulated differences on each branch from their merge base. git tries to put those two sets of accumulated changes together, and it has no notion of "branch primacy" or anything like that.

git does not, ever, lose commits. Go back to the basics, chase links from there, and pay particular attention to where the docs call out differences from other vcs's.

Merge is not time-based, and the result1 of a merge does not depend on which branch is "merged from" and which one is "merged into".2

The short form of "how merge works" is this. You are on some (current / "ours" / "merge-into") branch and you issue the command git merge <commit-ID>,3 so git does this:

  1. Find the "merge base" of the current branch, and the named commit.

    In your diagram, you're either on branch final, whose tip commit is i, or you're on branch master, whose tip commit is h. If you're on branch final you name commit h, and if you're on branch master, you name commit i, so that either way the two commits in question are h and i, and the merge-base is commit c: that's where you find common ground when working backwards from both of these commits.

  2. Run git diff (with -M option) to compare the merge-base with the current tip (e.g., c vs h).

  3. Run git diff (again with -M) to compare the merge-base with the other commit (e.g., c vs i).

  4. Using the diff in step 3—the changes "they" made—remove from it any of the changes already found in step 2, then make all the same changes in the current tip (e.g., make all changes not already contained in h, applying them to the tree in h).

Conflicts occur when, while trying to apply the "remaining changes" from step 4, the surrounding context does not match up (or, for file create, delete, or rename, there's no obvious right choice).

If there are no conflicts and you have not used --no-commit, git then commits the resulting tree, which of course goes on the current ("ours") branch. But you get the same tree either way. (Of course, if there are conflicts, the tree you get depends on how you resolve them. I just mean the automated result.)


1More precisely, the tree result. The commit that is made does depend on which branch is the "from" and which is the "into": in particular, the first-parent of the resulting merge commit is the "into" or "ours" branch, and the second parent is the "from" or "theirs" branch. And of course the commit itself is placed on the "into" / "ours" current branch.

2Barring options like --ours, -X ours, and the like, anyway.

3With git merge branchname, the branchname part is resolved to a commit-ID. Git does retain the original argument for the merge commit message, too, though, so a branch name is usually nicer than a raw SHA-1.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top