helion3
helion3

Reputation: 37501

Subtle difference with git pull --rebase and git rebase?

Despite all descriptions, git pull --rebase works differently than git fetch/git rebase [branch]. git pull --rebase is described as an alias for the fetch+rebase command, but I'm trying to find out how else they differ?


In another post, I was given git pull --rebase as a solution to a rebase problem in which git wasn't properly handling commits which have changed hash values due to merge conflict resolution on an upstream feature branch.

Until now, we've been using a combination of git fetch upstream and git rebase upstream/a-feature-branch.

However, when done this way, it acted like any commits that no longer match the hashes on the upstream branch were new work. It tried to re-apply them and caused merge conflicts:

$ git fetch upstream
-- no results, already fetched this morning
$ git rebase upstream/a-feature-branch
First, rewinding head to replay your work on top of it...
Applying: D-06437 (note: this commit already exists, but a merge conflict upstream has changed its hash)
Using index info to reconstruct a base tree...
...
Falling back to patching base and 3-way merge...
Auto-merging (file)
CONFLICT (content): Merge conflict in (file)
Failed to merge in the changes.
Patch failed at 0001 D-06437

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

imac: projectName ((c1452be...)|REBASE) $

However, running a pull instead achieves what we want:

$ git pull --rebase upstream a-branch-name
 * branch            a-branch-name -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: B-07241

This solution doesn't cause any conflicts and has properly updated the history with the modified commit/hashes that upstream has.


Update #1:

git pull --rebase upstream feature-branch-name equates to: git-rebase --onto c1452be62cf271a25d3d74cc63cd67eca51a127d 634b622870a1016e717067281c7739b1fe08e08d

Here are the three most recent commits on the developers work branch:

92b2194 Rick B-07241
634b622 Sue Merge pull request #254 from dboyle/B-07290
bc76e5b Bob [B-07290] Order Parts Ship To/Comments

And the most recent commit on the "new" feature branch:

c1452be Sue [B-07290] Order Parts Ship To/Comments

Note: The "merge" commit has been lost and the "Order Parts" commit now shows as being done by Sue, not Bob. I'm trying to confirm, but either someone cherry-picked the commit, or somehow ran a rebase in a way that discarded the merge commits.

Here are several variables that git-rebase.sh is using during each. The only difference is the onto:

"git-rebase" Variables during `git pull --rebase upstream feature-branch-name`
orig_head = 92b2194e3adc29eb3fadd93ddded0ed34513d587
onto_name = c1452be62cf271a25d3d74cc63cd67eca51a127d
onto = c1452be62cf271a25d3d74cc63cd67eca51a127d
mb = 438cc917c6f517913c9531e0a38f308d3aa13f0b
revisions = 634b622870a1016e717067281c7739b1fe08e08d..92b2194e3adc29eb3fadd93ddded0ed34513d587


"git-rebase" Variables during `git rebase upstream/feature-branch-name`
orig_head = 92b2194e3adc29eb3fadd93ddded0ed34513d587
onto_name = upstream/PartsInterface_E-01960
onto = c1452be62cf271a25d3d74cc63cd67eca51a127d
mb = 438cc917c6f517913c9531e0a38f308d3aa13f0b
revisions = c1452be62cf271a25d3d74cc63cd67eca51a127d..92b2194e3adc29eb3fadd93ddded0ed34513d587

The revisions calculated by "git rebase" are different than git pull.

Note: 634b6228 is a merge commit that only exists on the local branch, it no longer exists upstream.

Upvotes: 2

Views: 597

Answers (2)

torek
torek

Reputation: 490078

The answer is version-dependent, because the implementation of git pull --rebase and git rebase (plain rebase, without a lot of specific arguments) has evolved rather a lot between git 1.7 and git 2.3.

In general, the more modern your git version, the less difference there should be (I can't say "is", just "should be", :-) in part because I haven't followed the exact path of development over time). Here's the general thrust though:

  • With no arguments, git rebase looks for the "upstream" automatically. The definition of "upstream" in this case is the same as what git pull uses, so if you can git pull --rebase (with no additional arguments), there must be an automatic upstream.

  • When you run git pull --rebase (with no additional arguments), this first runs a git fetch that brings over new commits as usual. At this point—when you have the new commits, and also all old commits because you haven't yet updated the remote-tracking branch—it is easy to detect an unusual condition that does actually occur, namely, an upstream "history rewrite".

  • Since it was easy to detect, old versions of git do/did detect this during git pull --rebase and would automatically compensate for the rebase. (Those old versions of git, however, would (deliberately) fail to update remote-tracking branches. As a result this detection was itself somewhat limited as well.)

  • When git fetch was changed in git 1.8.4 to update remote-tracking branches even when invoked by git pull, the case that git pull could easily detect, became harder to detect again. If, however, your remote-tracking branches have reflogs (and usually they do), the reflogs could supply the information. So the pull script, and git in general, was modified / enhanced to use the reflogs to extract the "fork point" information (see the --fork-point section in the git merge-base documentation).

Given the ability to find a "fork point", git rebase can (and does, in recent versions of git) employ the same magic as git pull --rebase, so there should be no difference in behavior. Depending on which version of git you have (between 1.7 and 2.3), however, git pull may be finding the correct "fork point" while git rebase is not. And given your note:

Applying: D-06437 (note: this commit already exists, but a merge conflict upstream has changed its hash)

there was in fact an upstream "history rewrite", so you need this new "smarterized" version of git rebase to automatically discover it. The older, dumber/simpler rebase simply assumes that if you have this sequence of commits in your repository "now" (after fetching):

        OldD         <-- (origin/branch used to be here)
      /      \
* - *       B07241   <-- branch
      \
        D06437       <-- origin/branch (now)

and you run git rebase, then you want to take commits OldD and B07241 and apply them on top of D06437. The conflict occurs when trying to cherry-pick OldD and apply the resulting diff to commit D06437.

It's easy to fix this in an interactive rebase because you can simply delete the "pick" line for the "old" D commit.

Upvotes: 2

NoDataFound
NoDataFound

Reputation: 11979

This will not address your question about differences between git rebase and git pull --rebase, since I don't know the technical details either (and I practically never use git pull) but I do know the following works:

  • you fetch your repository, making origin/branch target some new commit A.
  • your branch HEAD point to commit B.
  • your local branch and origin/branch share a common ancestor ancestor.
  • rebasing branch (git rebase origin/branch) can be done like this:

    git reset --hard origin/branch # your branch is now the same as origin
    git cherry-pick ancestor..B # now you pick up the commit between the two branches.
    

I noticed that git cherry-pick worked better than git rebase, but I think that's only because it reapply the commit instead of replaying the history.

Upvotes: 0

Related Questions