Tim
Tim

Reputation: 99408

Does git pull check out the current branch after its merge/rebase step?

The merge/rebase step of git pull merges/rebases the remote tracking branch into/onto the current branch.

I wonder if git pull then checks out the current branch into the working directory? Or should I run check out myself?

My question is from my thought that the merge step modifies the current branch in the git repository, while the working directory isn't up to date with the current branch yet, so there is a need to check out the modified current branch into the working directory.

But I am not sure if the merge step of git pull does that already or if I should do it myself after git pull.

Upvotes: 0

Views: 83

Answers (2)

torek
torek

Reputation: 487775

The short answer is "no", and that's actually a complete and technically correct answer, but a bit unsatisfying and quite probably misleading: in all the good cases, the final state is as if git had done a checkout. Still, you can stop here if you like. :-)

The long answer is a bit long and gets complicated in modern versions of git, because you can set up git rebase to "autostash". But once again we come back to the fact that git pull runs git fetch followed by either git merge or git rebase.

  • The fetch step is simpler because it does not need to know or care about the work-directory. It contacts the named remote (taken from the current branch's branch.$branch.remote if you didn't specify one), gives that remote some refspec(s) (from additional arguments you passed, or your configuration if you didn't pass any), and brings over commits and other objects from the remote as needed. These commits and other objects are shoved into storage in your repository, where they're safely out of the way but available at any time. Some "remote-tracking" branch name(s) may be updated, and in any case the branch head(s) brought over are put into the special FETCH_HEAD file (in particular, FETCH_HEAD acquires the SHA-1 IDs that go with each branch head name).

    To see the whole name/SHA-1 ID mapping thing in action, run git ls-remote remote (e.g., git ls-remote origin). This contacts the remote and gets from it the same information that git fetch gets, then prints it out in your command window. It makes no changes to anything else so it just uses a bit of network bandwidth, and it's quite educational, in my opinion. (If there are a lot of branches and tags on the remote you may need to scroll back and forth to see all the output, or save the output in a file or whatever.)

  • The merge or rebase step is where the tricky bits are. Let's exit the nesting and consider the two. Let's also consider only the case of a merge with a single target (i.e., a normal everyday merge, not one of git's fancy octopus merges).

If you start with a clean working tree (and index), it's a lot easier to describe what happens now. Let's assume a clean working tree to begin with.

The fetch step brought over some commit(s). The merge or rebase finds the appropriate merge-base or rebase destination and then performs either a merge, or a rebase, as directed:

  • For a merge, git finds the merge base. Normally, this is a single specific commit. (In some cases there are multiple merge-base candidates, and git handles these too, but let's not worry about them.) You can think of this as the point at which your branch and the other branch diverged:

    <--older--time increases as we move right--newer-->
    [see footnote 1]
    
                  o - o       <-- your branch (master)
                /
    ... - o - *
                \
                  o - o - o   <-- their branch (origin/master)
    

    In this diagram, each little o node denotes a commit. The merge-base is the commit marked * instead of o. The tip-most commit of your branch master is the right-most o on the top line, and the tip-most commit of their branch origin/master is the right-most o on the bottom line.

    Let's call the tip-most commit of your branch A, just for concreteness. (It has its own unique SHA-1 ID and that's its "real name", but let's just call it A.) Let's call the merge base B and the tip of their branch C, and redraw that diagram:

                  o - A       <-- your branch (master)
                /
    ... - o - B
                \
                  o - o - C   <-- their branch (origin/master)
    

    To perform a merge (when a merge is required), git starts by running a diff between commit B and commit A, as if you did git diff B A (filling in the actual SHA-1 for B and A of course; and it uses an internal version of its diff code, rather than simply reading through literal git diff text output). This diff tells git "what you changed" since the common base B.

    Next, git runs a git diff B C (internal format again of course). This diff tells git "what they changed" since that same common base.

    Finally, git tries to combine the two diffs. If you added a file and they didn't, git keeps your added file. If you changed the spelling of neighbor to neighbour in file README and they didn't, it keeps your change. If they removed a file you didn't touch, or removed a line from a file you didn't touch, git keeps their change. If you both made the exact same change to weather.h, git keeps one copy of that change (rather than two copies). Git applies all the "kept" changes to the base version and writes the result to your work-tree, and the merge is now done with no merge conflicts. The result, git believes, is ready to commit. (Note that this is true even if some change you made requires, say, that the system keep the file they deleted, which git has now deleted. That is, suppose they said "oh look, this file isn't used, let's remove it" and you said "oh, oops, I forgot to use that file, let's use it". Git doesn't know that these changes actually conflict: all it can tell is that the lines in the diff don't collide.)

    The next step depends on whether you gave the --no-commit flag.2 This suppresses the commit, no matter what. If you didn't suppress the commit, git merge commits this no-apparent-conflict set of changes, as a "merge commit" that has two parents:

                  o - A ----- M   <-- your branch (master)
                /            /
    ... - o - B             /
                \          /
                  o - o - C   <-- their branch (origin/master)
    

    At this point, the working tree matches the new commit M. While it wasn't checked out, it was checked in (committed) as a merge.

    If you did say --no-commit, or if there was a merge conflict, the merge stops and leaves you to fix and/or commit the merge. The changes are simply present in your work-directory. If and when you decide to commit the result, that will be a merge commit. Git knows this because it leaves a trace-file behind saying "in the middle of a merge". If you decided to cancel the merge with git merge --abort, that removes the trace-file.

  • For a rebase, git goes through a similar process. However, instead of finding a merge-base, it finds the set of commits that you have that the upstream doesn't. We can draw that diagram yet again, but let's use different node names:

                  A - B       <-- your branch (master)
                /
    ... - o - o
                \
                  C - D - E   <-- their branch (origin/master)
    

    In this case git finds your two commits (A and B) and copies them (using git cherry-pick, more or less). The mechanism for this is potentially complicated, so let's just note here that each cherry-pick operation requires updating your work-tree and making a new commit. If all goes well, we calll the copy of A, A', and the copy of B B'; git moves the branch name to point to B'; and the final drawing looks like this:

                  A - B       [abandoned]
                /
    ... - o - o               A' - B'   <-- your branch
                \           /
                  C - D - E   <-- their branch
    

    Because each cherry-pick step updates your work-tree and makes a commit from it, your work-tree matches the final commit B', not because it got checked out, but because it got checked in (committed), exactly as in the merge case.

If you start with a dirty working tree (or "dirty index", i.e., any changes that are not checked in), it's a bit tougher to describe, but not too much so. We still have the same two cases:

  • For a merge, git will still attempt to combine your changes and their changes, but it will attempt to do that while keeping your additional uncommitted changes in place. If it succeeds, it makes a commit from the result as usual, unless suppressed as usual. If it fails, it stops as usual and leaves you to clean up the mess.

    The good thing about this is that you can sometimes produce a working merge even if merging from a clean work-tree would fail. The bad thing about this is that it's really easy to do it by accident, when you didn't mean to, and wind up with your changes mushed together with their changes and very hard to back out. It's generally just a bad idea: commit or stash, then merge from a clean state, maybe with --no-commit if you like. You can then run tests and do your own commit after doing any fixing-up. (Note that you can also use git commit --amend on merge commits.)

  • For a rebase, normally git rebase will simply refuse to run if the work-tree is not clean. That makes this an easy case. However, since git 1.8.4, rebase now has a configurable rebase.autostash setting. If you set this, your rebase will run git stash for you in this case, then do the rebase, then (if all goes well) pop the stash for you. This mode has an annoying (but not fatal) bug that was not fixed until git 2.0.1, and I recommend against it in general—I think it's better to just make a checkpoint commit, and clean it up in a later cleanup pass (it's usually good to have a cleanup pass before "finalizing" commits, and that's the time to squash away temporary checkpoints; until then, they rebase naturally, without any danger of goofed-up autostashes).


1Although the diagram says "time increases to the right", technically it is really just that nodes to the right are successors of their nodes to the left. This is especially true when you fetch commits from a remote. The time-stamps in those commits were made on some other computer, and even if you're good about keeping your own computer synched with an atomic clock somewhere, who knows if their clock(s) is/are/were correct? Those time stamps could be completely broken. The time order is not guaranteed, just "likely".

2Besides --no-commit, git merge also takes a --squash flag, which has two side effects: (1) the final commit is not a merge commit at all and (2) after doing all the usual work for a merge, the git merge stops without committing (just as for --no-commit). That is, what --squash does internally is to stop the final commit and stop git merge from writing that special status-file that tells git that it's in the middle of a merge. That way, the manual commit you must do afterward is an ordinary non-merge commit.

Upvotes: 2

Joseph Silber
Joseph Silber

Reputation: 219920

The current branch is checked out in the working directory.

After the merge/rebase, the branch has moved forward to a different commit.

So yes, git will checkout that commit, so that you stay on the current branch.

Upvotes: 2

Related Questions