threads / discuss / 43753

Re: [DRAFT] Branching and merging with git

Subject: Re: [DRAFT] Branching and merging with git

## tl;dr

3 messages between Nov 17, 2006 and Nov 17, 2006.

replies: 2people: 2as markdown or json

Junio C Hamano· Nov 17, 2006, 01:09 UTC · lore
linux@horizon.com writes:
> One outstanding problem with git's man pages is that often the most detail
> is in the command page that was written first, not the user-friendly
> one that you should use.

This is a very important point to remember not for users but for us in git community. Thanks for writing it down.

Show 7 quoted lines
> * Git's representation of history
>...
> And then there are commits which have multiple parents.  Two is most
> common, but git allows many more.  (There's a limit of sixteen in the
> source code, and the most anyone's ever used in real life is 12, and
> that was generally regarded as overdoing it.  Google on "doedecapus"
> for discussion of it.)
Dodecapus would find a few, no hits on doedecapus ;-).
Show 5 quoted lines
> * Deleting branches
>
> "git branch -d <head>" is safe.  It deletes the given <head>, but first
> it checks that the commit is reachable some other way.  That is, you
> merged the branch in somewhere, or you never did any edits on that branch.

It is not "somewhere" but "in the current branch", so in a sense it is a bit stricter than that. While on 'master' "branch -d topic" would not remove the topic branch head if it is not fully merged to my 'master' so that is a reasonable safety measure, but when I am on 'next' it will happily remove it. It is recoverable because it is reachable from 'next', though.

> * Remote tags
>
> TODO: Figure out how remote tags work, under what circumstances
> they are fetched, and what git does if there are conflicts.

One bug in my previous response is that I said we do this only when the command was invoked with shorthand remote name. Not so. We do this only when we use tracking branches.

The reason is because 'git pull $url $branch' (typical Linus's use) and 'git pull' (defaulting to 'origin' and using the tracking branch mapping stored in .git/remotes/origin prepared by git-clone) are sign of very different workflows. The former tends to be a one-shot event while the latter is most often synchronizing with either an upstream or a common distribution point (i.e. shared central repostiory). When you are fetching from somebody in a one-shot manner, most likely as a part of 'pull', you do not want to get the tag the other person has made to mark his private work in progress. But in the latter case, the other end is where everybody who works in the same area fetches from, and sharing the tags found there among the developers by default is desirable, and more importantly there is no risk of accidentally getting private tags, since the other end is a public distribution point and by definition should not have private tags that would clutter your refs/tags hierarchy.

Show 16 quoted lines
> * Exchanging work with other repositories, part II: git-push
>...
> You have to merge the changes locally, and re-try the push when you've
> got a new head that includes the most recently pushed work as an ancestor.
>
> This is exactly like "cvs commit" not working if your recent checkout
> wasn't the (current) tip of the branch, but git can upload more than
> one commit.
>
> The simplest way to resolve the conflict is to merge the remote head with
> your local head.  This is easiest if you have different local branches
> for fetching the remote repository and for pushing to it.
>
> That is, you have one head that just tracks the master repository's
> main branch, and another that you add your work to, and push from.
> This makes merging simpler when there are conflicts.

Here you _might_ want to mention an alternative workflow that uses rebase, which seems to be the way Wine folks run their project. Talking about all the different possibilities tends to cloud things and may not add value to the document, so I am just mentioning it as a possibility but I do not know if talking about rebase is useful in the context of this document.

Show 9 quoted lines
> * Merging (finally!)
>
> I went through everything else first because the most common merge case
> is local changes with remote changes.  Not that you can't merge two
> branches of your own, but you don't need to do that nearly as often.
>
> The primitive that does the merging is called (guess what?) git-merge.
> And you can use that if you want.  If you want to create a so-called
> octopus merge, with more than two parents, you have to.

This is not true; "git pull . topicA topicB topicC" works as expected. But we probably would not want to even talk about Octopus in a document like this. It is a curosity, and sometimes tends to make histories even less cluttered, but otherwise it does not add much value.

Show 7 quoted lines
> However, it's usually easier to use the git-pull wrapper.  This merges
> the changes from some other branch into the current HEAD and generates
> a commit message automatically.
>
> git-merge lets you specify the commit message (rather than generating it
> automatically) and use a non-HEAD destination branch, but those options
> are usually more annoying than useful.

I haven't tried for a long time, but I do not think non-HEAD destination even works at all. It might be better not to even mention git-merge at this point of the document.

Show 8 quoted lines
> * How merging operates
>...
> If all three of a given file in O, A, B are different, then the three
> versions are pulled into the index file, called "stage 1", "stage 2",
> and "stage 3", and a merge strategy driver is called to resolve the mess.
> Git then uses the classic line-based three-way merge, looking for isolated
> changes and applying the same rules as for files when two of the source
> files are the same in some range.

You might also want to mention that recursive first 3-way merges the renames. If O->A renames a path while O->B keeps it, the resulting stages are written under the new name.

Show 25 quoted lines
> * When merging goes wrong
>...
> and the result of "git diff" is
>
> diff --cc hello.c
> index 4b7f550,948a5f8..0000000
> --- a/hello.c
> +++ b/hello.c
> @@@ -3,5 -3,6 +3,10 @@@
>   int
>   main(void)
>   {
> ++<<<<<<< HEAD/hello.c
>  +      printf("Goodbye, cruel world!");
> ++=======
> +       printf("Hello, world!\n");
> +       return 0;
> ++>>>>>>> edadc53fc7a8aef2a672a4fa9d09aa16f4e14706/hello.c
>   }
>
> Notice how this is not a standard diff!  It has two columns of diff
> symbols, and shows the difference from each of the ancestors to the
> current hello.c contents.  I can also use "git diff -1" to compare
> against the common ancestor, or "-2" or "-3" to compare against each of
> the merged copies individually.

Another tool to help the user decide how the mess should be sorted out is "git log --merge -- $path". It gives the logs of commits that touched the path while the two branches were forked.

> * Cherry picking
>...
> You van get the list of commits on a branch with git-log or git-rev-list,
s/van/can/
> * Rebasing
>...
> Git-rebase can also help you divide up work.  Suppose you've mixed up
> development of two features in the current HEAD, a branch called "dev".

Ancestry graph before and after this procedure would help the reader a lot here.

>...
> This will find all patches that are in dev and not in dev1,
> apply them on top of master, and call the result dev2.
Show 18 quoted lines
> * Experimenting with merging
>...
> $ git status
> Hey, look, lots of interesting stuff.  Particularly, see
> # Changed but not updated:
> #   (use git-update-index to mark for commit)
> #
> #       unmerged: Makefile
> #       modified: Makefile
> #       unmerged: builtin.h
> #       modified: builtin.h
> #       unmerged: git.c
> #       modified: git.c
>
> The "unmerged" (a.k.a. "staged") files are ones that need manual resolution.
>
> (I notice that update-index.c isn't listed, despite being mentioned
> as a conflict in the message.  Can someone explain that?)

They were conflicts during the virtual ancestor computation by recursive (the merge between 'a' and 'b' commits in your earlier example). When a virtual ancestor is created, it can textually have conflicted merge, but that is recorded along with conflict markers without manual resolving for obvious reasons. If two branches that use the virtual ancestor modifies the conflicted region the same way (because they needed to resolve that conflict in their branch), the final 3-way merge that uses the virtual ancestor as the merge-base will replace that conflicted region with their changes. This "even conflict markers can be eliminated by a merge resolution" behaviour is what inspired git-rerere, by the way.

If you are using this particular commit as an example, you might also want to tell your readers about:

	git show -M 3f69d405

(-M is there to make the output more readable, because this merge involved a few renames).

linux@horizon.com· Nov 17, 2006, 03:17 UTC · re: Junio C Hamano · lore

Overall, thank you. I'm trying to merge all your comments into the document to make it better, but there are enough that it's taking me a while.

Show 6 quoted lines
>> One outstanding problem with git's man pages is that often the most detail
>> is in the command page that was written first, not the user-friendly
>> one that you should use.
> 
> This is a very important point to remember not for users but for
> us in git community.  Thanks for writing it down.

There's a great example coming up, in the git-show example you gave me. That's a very sparse man page...

> Dodecapus would find a few, no hits on doedecapus ;-).
Wups, thanks.
Show 12 quoted lines
>> * Deleting branches
>>
>> "git branch -d <head>" is safe.  It deletes the given <head>, but first
>> it checks that the commit is reachable some other way.  That is, you
>> merged the branch in somewhere, or you never did any edits on that branch.
> 
> It is not "somewhere" but "in the current branch", so in a sense
> it is a bit stricter than that.  While on 'master' "branch -d
> topic" would not remove the topic branch head if it is not fully
> merged to my 'master' so that is a reasonable safety measure,
> but when I am on 'next' it will happily remove it.  It is
> recoverable because it is reachable from 'next', though.

Oh! Thanks for the info. The limitation makes a certain amount of sense, and as I'd never run into it, I'm not going to complain.

Show 16 quoted lines
> The reason is because 'git pull $url $branch' (typical Linus's
> use) and 'git pull' (defaulting to 'origin' and using the
> tracking branch mapping stored in .git/remotes/origin prepared
> by git-clone) are sign of very different workflows.  The former
> tends to be a one-shot event while the latter is most often
> synchronizing with either an upstream or a common distribution
> point (i.e. shared central repostiory).  When you are fetching
> from somebody in a one-shot manner, most likely as a part of
> 'pull', you do not want to get the tag the other person has made
> to mark his private work in progress.  But in the latter case,
> the other end is where everybody who works in the same area
> fetches from, and sharing the tags found there among the
> developers by default is desirable, and more importantly there
> is no risk of accidentally getting private tags, since the other
> end is a public distribution point and by definition should not
> have private tags that would clutter your refs/tags hierarchy.
I'll work this in somehow, thanks.
Show 6 quoted lines
> Here you _might_ want to mention an alternative workflow that
> uses rebase, which seems to be the way Wine folks run their
> project.  Talking about all the different possibilities tends to
> cloud things and may not add value to the document, so I am just
> mentioning it as a possibility but I do not know if talking
> about rebase is useful in the context of this document.
Done, thanks.
Show 9 quoted lines
>> The primitive that does the merging is called (guess what?) git-merge.
>> And you can use that if you want.  If you want to create a so-called
>> octopus merge, with more than two parents, you have to.
> 
> This is not true; "git pull . topicA topicB topicC" works as
> expected.  But we probably would not want to even talk about
> Octopus in a document like this.  It is a curosity, and
> sometimes tends to make histories even less cluttered, but
> otherwise it does not add much value.
Sorry; the SYNOPSIS line for git-pull had me fooled.
Show 11 quoted lines
>> However, it's usually easier to use the git-pull wrapper.  This merges
>> the changes from some other branch into the current HEAD and generates
>> a commit message automatically.
>>
>> git-merge lets you specify the commit message (rather than generating it
>> automatically) and use a non-HEAD destination branch, but those options
>> are usually more annoying than useful.
> 
> I haven't tried for a long time, but I do not think non-HEAD
> destination even works at all.  It might be better not to even
> mention git-merge at this point of the document.
Well, I want to at least say "you think so, wouldn't you?"
Show 5 quoted lines
>> * How merging operates
> 
> You might also want to mention that recursive first 3-way merges
> the renames.  If O->A renames a path while O->B keeps it, the
> resulting stages are written under the new name.
Thanks!  I wondered how that happened!
>> * When merging goes wrong
> Another tool to help the user decide how the mess should be
> sorted out is "git log --merge -- $path".  It gives the logs of
> commits that touched the path while the two branches were forked.
The things I never knew about...
Show 5 quoted lines
>> Git-rebase can also help you divide up work.  Suppose you've mixed up
>> development of two features in the current HEAD, a branch called "dev".
> 
> Ancestry graph before and after this procedure would help the
> reader a lot here.

I figured the (excellent) pictures in git-rebase would save me the trouble, but yeah, I suppose so.

Show 12 quoted lines
> They were conflicts during the virtual ancestor computation by
> recursive (the merge between 'a' and 'b' commits in your earlier
> example).  When a virtual ancestor is created, it can textually
> have conflicted merge, but that is recorded along with conflict
> markers without manual resolving for obvious reasons.  If two
> branches that use the virtual ancestor modifies the conflicted
> region the same way (because they needed to resolve that
> conflict in their branch), the final 3-way merge that uses the
> virtual ancestor as the merge-base will replace that conflicted
> region with their changes.  This "even conflict markers can be
> eliminated by a merge resolution" behaviour is what inspired
> git-rerere, by the way.
Cool, thanks!  I added mention of this.
Show 7 quoted lines
> If you are using this particular commit as an example, you might
> also want to tell your readers about:
> 
> 	git show -M 3f69d405
> 
> (-M is there to make the output more readable, because this
> merge involved a few renames).

I'm wondering what the heck that does! I get a super-short diff with no mention of any renames at all. Is this passed on to git-diff-tree? What does "detect renames" mean if it doesn't tell me about them? I'm actually confused.

diff --cc builtin-read-tree.c index ec40d01,99e7c75..716f792

--- a/builtin-read-tree.c
+++ b/builtin-read-tree.c
@@@ -9,9 -9,9 +9,10 @@@
  
  #include "object.h"
  #include "tree.h"
+ #include "cache-tree.h"
  #include <sys/time.h>
  #include <signal.h>
 +#include "builtin.h"
  
  static int reset = 0;
Junio C Hamano· Nov 17, 2006, 05:55 UTC · re: linux@horizon.com · lore
linux@horizon.com writes:
Show 7 quoted lines
>>> * When merging goes wrong
>
>> Another tool to help the user decide how the mess should be
>> sorted out is "git log --merge -- $path".  It gives the logs of
>> commits that touched the path while the two branches were forked.
>
> The things I never knew about...

Of course "git log -p --merge -- $path" would give the patch text as well.

Show 12 quoted lines
>> If you are using this particular commit as an example, you might
>> also want to tell your readers about:
>> 
>> 	git show -M 3f69d405
>> 
>> (-M is there to make the output more readable, because this
>> merge involved a few renames).
>
> I'm wondering what the heck that does!  I get a super-short diff with
> no mention of any renames at all.  Is this passed on to git-diff-tree?
> What does "detect renames" mean if it doesn't tell me about them?
> I'm actually confused.

"show $merge" is really "diff-tree --cc -p $merge". So first I should (not necessarily "you should to the readers of this document") talk about three ways to describe a merge commit with textual diffs.

(1) N independent diffs between each of the parents and the child.
    We could get this with
    	git diff-tree -m -p $merge
    but it is mostly useless, because very often many paths in a
    merge are truly trivial and the version from one of the
    parents is taken verbatim, whole file.  When looking at the
    development history, the real reason of the change is found
    in earlier log for that parent, and not in the merge in
    question.
    The diff between the child and its first parent is somewhat
    useful, because it represents the damage inflicted on his
    branch the person saw when he made the merge.  For this
    reason, "--stat" gives the graph for the first-parent diff
    for a merge.  But otherwise "diff-tree -p" by default stays
    silent about merges because it is not that useful, and that
    is why the above asks for the "useless output ;-)" with an
    explicit "-m".
(2) Uncompressed "combined diff" between all parents and the child.
    We can get this with:
	git diff-tree -c -p $merge
    This gives a combined diff that shows all the files parents
    and child disagreed (in other words, if the resulting file
    matches verbatim with one of the parents, it is not shown).
    This is already useful by reducing the clutter of truly
    trivial merges, compared to (1) above, but most clean merges
    take either first or second parent's version verbatim for
    each hunk (but not necessarily taking all hunks from the
    same parent) and these hunks are not very interesting.
    Because "-c" explicitly tells something special to be done
    for a merge, you do not need to say "-m" for the above
    command (giving -m does not hurt, but is not necessary).
    Uncompressed combined diff is a still per-file affair, so by
    default the above gives the output in the "raw" format, and
    that is why the above command still says "-p".
(3) Compressed "combined diff".  This is what "show" gives by
    default, and we can get this with:
	git diff-tree --cc $merge
    The difference from "-c -p" output is that this reduces the
    clutter further by dropping uninteresting hunks from the
    output.  If all the changes in a hunk is from only one
    parent, or the changes are the same from all but one parent,
    the hunk is dropped from the output (looking at dodecapus
    with --cc is interesting for this reason).
    Like "-c", this explicitly asks a magic to be done on a
    merge, so "-m" is implied.  Unlike "-c", this operation is
    per-hunk and "raw" format (which is inherently per file)
    does not make any sense.  Because "raw" is impossible, "-p"
    is also implied.

Both (2) and (3) are "combined". It combines the diffs with each parent; for N-parent merge, it combines N diffs into one.

What -M/-C does is to see which path in each parent is used to diff against a path in the child. For example, the stat part of:

	git diff-tree --stat --cc -p -M 3f69d405

shows us that builtin-apply.c had a few insertion and deletion (remember, this is the diff between the first parent and the child -- "damage given to the first parent due to this merge").

If you run the above without -M, you will see a huge combined diff for builtin-apply.c because the second parent (i.e. the branch that was merged) did not have builtin-apply.c -- it still had the file under its old name, apply.c.

So what ends up getting combined without -M is diff between builtin-apply.c of the first parent and the child, and diff between /dev/null and builtin-apply.c of the child.

But with -M, what it combines is diff between builtin-apply.c of the first parent and the child, and diff between apply.c of the second parent and builtin-apply.c of the child. This obviously produces a lot more reasonable output -- actually the merge for this particular path is texually trivial that it does not even show in the --cc output. You can still view what it was by looking at:

	git diff-tree -c -p -M 3f69d405

Side note: I am not sure if the --cc hunk droppage logic is doing the right thing for the first hunk for builtin-apply.c case. I think it is an "interesting" hunk but somehow --cc output does not show it. The second and later hunks are definitely uninteresting, though.

← back to recent threads