David Casillas
David Casillas

Reputation: 1911

Sort Git commit history affecting 2 branches

Starting from this commit history, with two local branches a and b not pushed to origin:

C1 - C2 - C3 - C4 - C5
          |         |
          a         b

I want to end in this situation, where C2+C5 is a commit after C5 has been squashed into C2:

C1 - C2+C5 - C3 - C4 
              |   |
              a   b

The problem is that doing an interactive rebase both a and b branches diverge and there is no way to end in the above situation.

To do the change I have to remove a branch and recreate it after the rebase is performed. Is there a better way to do this?

Upvotes: 0

Views: 43

Answers (2)

torek
torek

Reputation: 488223

There are three things to know here1 as you go in:

  1. You can't actually change any commits. Internally, even Git can't do that. What Git must do here is to make new commits.

  2. Each commit has a unique "number": its hash ID. The new commits will have their own unique numbers, different from the old (still-extant) commits, which will remain in your repository for at least a little while.

  3. A branch name, like your a or b here,2 just remembers the raw hash ID of the last commit in some sequence. In your case, a is the last commit in your C1-C2-C3 sequence, and b is the last commit in your C1-C2-C3-C4-C5 sequence.

So what really happens is that you start with ... well, I'm going to draw the commits using single uppercase letters, rather than C1, C2, etc., which means I'll change the branch names as well, to branch-a and branch-b. We start with this:

A--B--C   <-- branch-a
       \
        D--E   <-- branch-b

We now keep original commit A but make a new commit F that is the result of turning B into changes (by comparing B vs A), and turning E into changes (by comparing E vs D), and combining those changes and applying them all to the snapshot in commit A:

  F   <-- HEAD
 /
A--B--C   <-- branch-a
       \
        D--E   <-- branch-b

(This uses Git's "detached HEAD" mode internally, during the rebase process, hence the use of the special name HEAD to locate commit F directly.) Having made this new commit, Git now has to copy existing commit C to a new commit G, by comparing C vs B to see what changed and applying those changes to F. The result is:

  F--G   <-- HEAD
 /
A--B--C   <-- branch-a
       \
        D--E   <-- branch-b

New commit G is where you'd like the name branch-a to point in the end, so let's keep on copying commits—there's one left to copy—to turn D into H but draw it up one more step:

       H   <-- HEAD
      /
  F--G
 /
A--B--C   <-- branch-a
       \
        D--E   <-- branch-b

That's the heart of the rebase process, for any kind of rebase: it copies commits, one at a time, as if by git cherry-pick. The interactive style rebase simply lets you take the set of pick commands—the individual cherry-pick instructions—and modify them to change the order or combine commits.3

In any case, having done the copying, the last few steps of git rebase involve yanking the branch name around to point to the last-copied commit. In this case, that's new commit H, resulting from cherry-picking old commit D. The special name HEAD identifies this commit right now, so Git runs the equivalent of:

git branch -f branch-b HEAD

to force the name branch-b to point here:

       H   <-- branch-b, HEAD
      /
  F--G
 /
A--B--C   <-- branch-a
       \
        D--E   [abandoned]

Then, Git re-attaches HEAD to the branch name, exiting the detached HEAD mode that it uses internally during rebasing:

       H   <-- branch-b (HEAD)
      /
  F--G
 /
A--B--C   <-- branch-a
       \
        D--E   [abandoned]

and now you see what you saw that prompted your question: is there some way to make Git move the name branch-a while it's doing all this?

The answer to that is no. Git does now have a fancier version of git rebase -i, which you invoke with git rebase -r. To make this fancier version work, Git has internally added all4 the tools it would need to make this a reality—but there's still some more work to do, to glue the tools together in a usable way. So for now, after you've done your git rebase -i, use eftshift0's final git branch -f command from his answer. That forcibly moves the other branch name, so that we get:

       H   <-- branch-b (HEAD)
      /
  F--G   <-- branch-a
 /
A--B--C
       \
        D--E   [abandoned]

Since the name branch-a is never actually deleted here, its reflog retains the fact that it used to point to commit C, just as branch-b's reflog retains the fact that it used to point to commit E.


1Well, there are even more things to know, but these are the three I'll call out, because they're particularly relevant.

2In general, it's wise to avoid building branch names with the digits 0 through 9 and/or letters a through f without also adding at least one other letter outside this range, or a hyphen, or something. The reason is that the commit "numbers" are spelled in hexadecimal, as a big ugly string like e9b77c84a0a0df029f2a3a8114e9f22186e7da24. This is composed entirely of characters from that [0-9a-f] set. If you keep the name short—three characters or fewer—you're "safe", so the branch names a and b themselves are fine, as are cab and bed. But once you get to four or more letters, such as cafe or deaf or decade: well, now Git may try that name as an abbreviated hash ID.

If a name works as one or more abbreviated hash IDs, that name can be considered ambiguous: did you mean the name, or the hash ID that that name is short for? Since the letter r cannot be in the set, the branch name beer is fine, but beef could be ambiguous.

In practice, this is rarely a problem,5 and if you hit an ambiguous case and Git treats it incorrectly, just spell out a longer version of the name: the branch name beef is really refs/heads/beef, which you can abbreviate to heads/beef if typing refs/heads/beef is too much.

3You can do more than just these two, of course. With some extra effort that Git could perhaps make a bit easier, you can split a commit into multiple separate commits. You can drop commits entirely. You can have Git run commands after a copy step. But these are the two you're using at the moment.

4Or maybe "most": the label commands get us most of the way there, but depending on things I'm pretty sure others have considered but nobody has yet implemented, it's not clear whether we might want one or two new script-level commands for this too.

5There just aren't that many words that are spelled with these. After "cafebabe" and "deadbeef", one has to get into 133t-5p34k or something. I came up with a fake headline though: FADED ABACA DEFACE CAFE FACADE.

Upvotes: 1

eftshift0
eftshift0

Reputation: 30212

You do an interactive rebase to reorder/squash

git checkout b
git rebase --interactice C1
# in the editor you move C5 right after C2
# you set C5 to squash
# leave C3 and C4 with pick after C5
# save and let it run
# when rebase is finished:
git branch -f a b~

And that's it

Upvotes: 1

Related Questions