Search code examples
gitmergegit-mergegit-rebasegit-pull

Is git pull --rebase the same as git pull when no work is done on the local master branch?


I understand that the difference between git rebase and git merge is that git rebase moves the beginning of a branch to the tip of another branch, where as git merge creates a new commit that represents (i.e. includes or integrates) branched commits from another branch. So for example:

Since git pull is equivalent to git fetch + git merge, and git pull --rebase is equivalent to git fetch + git rebase, let's say you have a local master branch and a remote master branch:

- o - o - o - H - A - B - C (master)
               \
                P - Q - R (origin/master)

HEAD is at C

If you git pull you end up with this:

- o - o - o - H - A - B - C - X (master)
               \             /
                P - Q - R --- (origin/master)

HEAD will now be at X which is commit R of the remote repository's commit history because the merge commit (X) represents (i.e. includes or integrates) the remote branch's commit history that was merged

If on the other hand you did git pull --rebase, you would end up with this:

- o - o - o - H - P - Q - R - A' - B' - C' (master)
                          |
                          (origin/master)

As you can see git pull is not the same as git pull --rebase because git pull --rebase gives HEAD at C' which is commit C of the local master branch, where as when git pull was used HEAD was at R. In other words, contents of the commit history via git pull are the same as the contents of the commit history from git pull --rebase, but the order of commits in the commit histories is different

But what happens when you are pulling from a remote branch when no work has been done on the local master branch:

local master branch and a remote master branch:

- o - o - o - H (master)
               \
                P - Q - R (origin/master)

HEAD is at H

If you git pull you end up with this:

- o - o - o - H - X (master)
               \             \
                P - Q - R --- (origin/master)

HEAD will now be at X which is commit R of the repo because the merge commit represents the remote branch that was merged

If on the other hand you did git pull --rebase, would you end up with this?:

- o - o - o - H - P - Q - R (master)
                          |
                          (origin/master)

Meaning that HEAD is at R which means that git pull --rebase is the same as git pull when no work is done on the local branch


Solution

  • When git pull runs git merge, it neither specifies --ff-only nor --no-ff, unless you specifically ask it to.

    [given the commit graph]

    ...--o--o--o--H   <-- master (HEAD)
                   \
                    P--Q--R   <-- origin/master
    

    [you suggest that git merge, and therefore git pull, would produce]

    ...--o--o--o--H---------X   <-- master (HEAD)
                   \       /
                    P--Q--R   <-- origin/master
    

    This is not the case—not by default, anyway. If you run git merge --no-ff origin/master, you do get this result. If you run git merge --ff-only, or allow the default action, you get instead:

    ...--o--o--o--H
                   \
                    P--Q--R   <-- master (HEAD), origin/master
    

    Git calls this a fast-forward. When it's specifically git merge that does this,1 Git calls it a fast-forward merge, although no actual merge takes place: Git really just does a git checkout of commit R while moving the branch name master so that it points to commit R (and leaving HEAD attached to branch name master).

    The --no-ff option tells Git: Do not do a fast-forward even if you can. The result is a new merge commit (or an error, or whatever else might go wrong, of course). The --ff-only option tells Git: If you can do a fast-forward, do it; if not, don't do anything, just error out.

    When you have git pull run git merge, you get the same set of options. If a fast-forward is (a) possible and (b) permitted, the result of the git merge is the same as that of a git rebase: the name master gets updated and nothing inside the commit graph changes in any way. If the fast-forward is not possible, or is inhibited via --no-ff, the result of a git merge differs from that of a git rebase in that there is a new merge commit X.

    The contents—the saved snapshot—of your new merge commit X match those of your commit R. But since R and X are different commits, they have different hash IDs, and subsequent Git operations will behave differently.


    1Both git push and git fetch can do this to references updated via refspecs. When git push moves the server's branch in a fast-forward manner, you generally see nothing special. When git push cannot move the branch this way, you normally get a rejection from the server. The git fetch annotation, rather than ! rejected (non-fast-forward), is (forced update). (Well, that's one of three (!) git fetch annotations.) See example immediately below.

    What I like to emphasize here is that the fast-forwarding is a property of the label motion. A fast-forward is possible if the commit identified before the motion—in this case commit H—is an ancestor of the commit to be identified afterward, i.e., commit R.

    Here's an example of a non-fast-forward fetch update. I have a local clone of the Git repository for Git. One of the branches in the upstream is named pu, which stands for proposed updates. This branch is regularly rewound and rebuilt with new commits: someone proposes some new feature, writes a first stab at it, and those go in for testing. Bugs get found, the feature gets reworked, and the commits that went in are discarded in favor of new improved ones that go in to the pu branch. So my origin/pu will be force-updated:

    $ git fetch
    remote: Counting objects: 509, done.
    remote: Compressing objects: 100% (214/214), done.
    remote: Total 509 (delta 325), reused 400 (delta 295)
    Receiving objects: 100% (509/509), 468.38 KiB | 1.52 MiB/s, done.
    Resolving deltas: 100% (325/325), done.
    From [url]
       d8fdbe21b5..95628af9bb  next       -> origin/next
     + b6268ac8c6...465df7fb28 pu         -> origin/pu  (forced update)
       8051bb0ca1..f0bab95d47  todo       -> origin/todo
    $ 
    

    Note that my Git updated my origin/next, my origin/pu, and my origin/todo. Two of these updates were fast-forward operations: commit d8fdbe21b5 (my old origin/next) is an ancestor of 95628af9bb (my updated origin/next), for instance. But b6268ac8c6, my old origin/pu, is not an ancestor of 465df7fb28. My Git updated my origin/pu anyway, losing access to commit b6268ac8c6 (except via reflogs).

    To announce this fact, git fetch:

    1. prefixed the line with +;
    2. printed three dots in the update, instead of two; and
    3. added (forced update) at the end of the line.

    Because the changes to origin/next and origin/todo were fast-forward operations, my Git said nothing extra about them.