2

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.

helion3
  • 34,737
  • 15
  • 57
  • 100

2 Answers2

2

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Just adding that I have git version 1.9.3 (Apple Git-50), though other people in my team may have different versions. The console output in my question is from git 1.9.3 – helion3 Mar 03 '15 at 01:22
  • Interesting ... poking about a bit more, `git rebase` began defaulting to `--fork-point` in git 1.9, so one might expect it to be there in that version. There was a bug in it that was fixed in git 2.1.0 though: see commit 1e0dacdbdb751caa5936b6d1510f5e8db4d1ed5f for specifics. – torek Mar 03 '15 at 01:35
  • I've upgraded to git version 2.2.1 and am still seeing the same issue. Looking at "git log" and "git cherry" comparisons of the developers branch and the feature branch, I can see 7 commits that are actually the same but have different hashes. I don't know what exactly happened to cause them to get new hashes. Is there a way I can "preview" or get verbose output of what "git pull" is comparing? I want to be able to see what it apparently sees – helion3 Mar 03 '15 at 17:53
  • Ok, so "git cherry upstream/feature-branch-name dev-branch` actually indicates four commits that have an equivalent upstream. However, these are not the four it tries to merge, so it can't tell the commits are essentially the same as what's on the feature branch. I suppose that means those commits changed somehow, likely a merge conflict resolution - it changed the contents of the commit slightly. – helion3 Mar 03 '15 at 17:57
  • Interesting. Take a look at `$(git --exec-path)/git-pull`, which is the actual pull script, and its usage of `$oldremoteref` (in git 2.2.0 this is line 254)... – torek Mar 03 '15 at 18:04
  • Also, I'm comparing the patch IDs and actual patch contents of the "old" commit versus the "new" commit. The hash changed because there are some small content differences - meaning I can tell someone had to fix a merge conflict during the rebase. However, I still have no idea why "git pull --rebase" works, but "git rebase" doesn't – helion3 Mar 03 '15 at 18:23
  • Is it possible that there's some "patch difference threshold" that git pull uses that git rebase doesn't? I'm comparing the changed commits and they're only different by a few lines (out of 1k line changes). Otherwise they're very similar. However, the patch IDs are totally different – helion3 Mar 03 '15 at 18:30
  • Again, look at the actual `git pull` shell script. (It's a shell script, you can copy it elsewhere and whack on it and run it and observe what it does! Just remember it's normally run from the exec-dir with the exec-dir in its path.) Its last step is to run an actual `git rebase` command: `eval "exec $eval"` where `$eval` is built out of `git-rebase $diffstat $strategy_args $merge_args $rebase_args ...` – torek Mar 03 '15 at 18:34
  • I've updated the question with some extra info that wouldn't fit here – helion3 Mar 03 '15 at 19:00
  • Ok, so `git rebase` uses an `onto_name=upstream/PartsInterface_E-01960` while the `git pull --rebase` uses `onto_name= c1452be62cf271a25d3d74cc63cd67eca51a127d` (which is the latest commit on that branch). – helion3 Mar 03 '15 at 19:24
  • Well they're officially pulling different revisions for rebasing onto. I have no idea why, but I'm emailing the official git mailing list. Thanks – helion3 Mar 03 '15 at 20:40
  • Seems that `--fork-point` is not the default when a custom upstream branch is named. Using `--fork-point` in the `git rebase` command also fixes this issue. – helion3 Mar 03 '15 at 22:45
  • Aha! I did see that the `rebase` script did auto-fork-point only under some conditions, but did not notice that you were giving it a specific `` argument. – torek Mar 04 '15 at 00:02
  • fork-point... we meet again: http://stackoverflow.com/a/20423029/6309 (it was improved in Git 2.1) – VonC Mar 04 '15 at 07:10
  • Note: git 2.8 (March 2016) will allow for a `git pull --rebase=interactive command`. See http://stackoverflow.com/a/29717607/6309. – VonC Jan 27 '16 at 09:39
0

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.

NoDataFound
  • 11,381
  • 33
  • 59