0

When I have 2 branches pointing at the same commit, and then rebase them both onto the same new base commit, why do the rebased branches diverge?

I expected that they'd replay in the same way, and end up pointing at the same new commit.

touch a; touch b; touch c
git add a
git commit -m 'a'
git add b
git commit -m 'b'
git checkout -b branch-01 HEAD^
git add c
git commit -m 'c'
git checkout -b branch-02
git rebase master branch-01
git rebase master branch-02
git log --all --graph --decorate --pretty=oneline --abbrev-commit
cp.engr
  • 2,291
  • 4
  • 28
  • 42

2 Answers2

3

To explain what happened, try this as an experiment:

$ git checkout -b exp1 master
<modify some file; git add; all the usual stuff here>
$ git commit -m commit-on-exp1

At this point you have an experimental branch named exp1 with one commit that's not on master:

...--A--B   <-- master
         \
          C1   <-- exp1

Now we'll make an exp2 branch pointing to commit B, and copy commit C1 to a new commit C2 on branch exp2:

$ git checkout -b exp2 master
$ git cherry-pick exp1

The result is:

          C2   <-- exp2
         /
...--A--B   <-- master
         \
          C1   <-- exp1

Now let's repeat with exp3, creating it so that it points to commit B and then copying exp1 again:

$ git checkout -b exp3 master
$ git cherry-pick exp1

Do you expect exp3 to point to commit C2? If so, why? Why did exp2 point to C2 rather than to C1 like exp1?

The issue here is that commits C1 and C2 (and now C3 on exp3) are not bit-for-bit identical. It's true they have the same snapshot, the same author, the same log message, and even the same parent (all three have B as their one parent). But all three have different committer date-and-time-stamps, so they are different commits. (Use git show --pretty=fuller to show both date-and-time-stamps. Cherry-pick, and hence rebase too, copies the original author information including date-and-time, but because it's a new commit, uses the current date-and-time for the committer timestamp.)


When you use git rebase, in general, you have Git copy commits, as if by cherry-pick. At the end of the copying, Git then moves the branch name so that it points to the last copied commit:

...--A--B   <-- mainline
      \
       C--D--E   <-- sidebranch

becomes:

          C'-D'-E'  <-- sidebranch
         /
...--A--B   <-- mainline
      \
       C--D--E

Here C' is the copy of C that's changed to use B as its parent (and perhaps has a different source snapshot than C), D' is the copy of D, and E' is the copy of E. There was only one name pointing to E; that name is now moved, so there is no name pointing to E.

But if you have two names pointing to E originally, one of those two names still points to E:

          C'-D'-E'  <-- sidebranch
         /
...--A--B   <-- mainline
      \
       C--D--E   <-- other-side-branch

If you ask Git to copy C-D-E again, it does that—but the new copies are not C'-D'-E' because they have new date-and-time stamps. So you end up with what you saw.

Hence, if you want to move two or more names while copying some chain of commits, you'll be OK using git rebase to move the first name, but you will have to do something else—such as run git branch -f—to move the remaining names, so that they point to the commit copies made during the one rebase.

(I've always wanted to have a fancier version of git rebase that can do this automatically, but it's clearly a hard problem in general.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • `all three have different date-and-time-stamps, so they are different commits.` I think this is the crux of your explanation, and it would make sense, but `git log` shows me that both rebased commits have the same timestamp. – cp.engr Jul 06 '19 at 20:22
  • Also, the only differences in the outputs from `git show` on the 2 rebased commits are the commit IDs and the branch names. – cp.engr Jul 06 '19 at 20:30
  • 1
    Use `git show --pretty=fuller` so that you see *both* time stamps. There must be some difference: different parent, or different author, committer, tree, or log message. If there is no difference at all, the commit is literally the same commit and thus has the same hash ID and there aren't two separate commits. – torek Jul 06 '19 at 20:53
  • Ah! `git show --pretty=fuller` shows that the the `AuthorDate`s are the same, but the `CommitDate`s differ. That explains it. Could you update your answer to make this more explicit? – cp.engr Jul 06 '19 at 21:05
  • Also, I'd rather see just about all of the `cherry-pick` stuff in your answer go away, as IMO it just adds clutter, and I already understood that you can use it to the same effect as a `rebase`. (i.e. My answer to your questions, "Do you expect exp3 to point to commit C2? If so, why?" was yes, because I was not aware of any metadata to create the difference.) – cp.engr Jul 06 '19 at 21:05
  • I don't have time right now for a fancier rewrite, and cherry-pick (which rebase uses internally) is what copies the author date stamp, so I'll just note that it's the committer date stamp here... – torek Jul 06 '19 at 21:08
  • Good edits, thanks for those. Any additional updates you make need not be immediate. But note I was asking for simpler, not "fancier". What I mean is IMO, this would be better if you got rid of *everything* before the sentence beginning "The issue here...". I think your new addition makes the point. – cp.engr Jul 06 '19 at 21:20
0

Why the Branches Diverged

Among the metadata used to calculate the hash for a git commit, not only is there an Author and an AuthorDate; there is also a Committer and a CommitterDate. This can be seen by running e.g.

git show --pretty=fuller branch-01 branch-02

Each rebase (or cherry-pick) command updates the committer date in the new commit(s) according to the current time. Since the two rebases in the question were performed at different times, their CommitterDates differ, thus their metadata differ, thus their commit hashes differ.

How To Move Branches/Tags Together

torek correctly notes that

if you want to move two or more names while copying some chain of commits, you'll be OK using git rebase to move the first name, but you will have to do something else—such as run git branch -f—to move the remaining names, so that they point to the commit copies made during the one rebase.

About Author vs Committer

From Difference between author and committer in Git?:

The author is the person who originally wrote the code. The committer, on the other hand, is assumed to be the person who committed the code on behalf of the original author. This is important in Git because Git allows you to rewrite history, or apply patches on behalf of another person. The FREE online Pro Git book explains it like this:

You may be wondering what the difference is between author and committer. The author is the person who originally wrote the patch, whereas the committer is the person who last applied the patch. So, if you send in a patch to a project and one of the core members applies the patch, both of you get credit — you as the author and the core member as the committer.

cp.engr
  • 2,291
  • 4
  • 28
  • 42