Home > Enterprise >  How do I use git rebase to move a commit?
How do I use git rebase to move a commit?

Time:10-25

This is the state of my local repo. I was at AAAAA and made commit CCCCC. I did a git pull and it pulled the commits and did an auto(ish) merge of BBBBB into CCCCC and made DDDDD. I don’t want that, so I killed DDDDD with a git reset.

$ git tree --all

CCCCC (HEAD, main) foobar issue 666
| * BBBBB (tag: fubar, origin/main, origin/HEAD) fubar issue #69
|/  
* AAAAA foo

Instead of a merge, I want to move CCCCC onto BBBBB. How do I rebase this? Do I need to do a switch or checkout to BBBBB first?

* CCCCC (HEAD, main) foobar issue 666
* BBBBB (tag: fubar, origin/main, origin/HEAD) fubar issue #69
* AAAAA foo

CodePudding user response:

Rebase is correct:

git fetch
git switch main
git rebase origin/main

You could probably just say git pull --rebase instead, but I'm not one to take chances.

CodePudding user response:

Technically, you don't actually move a commit. Instead, you copy it to a new and improved commit (with different hash ID). This is true in Mercurial too. However, the Mercurial interface for rebasing ("grafting") and history editing (hg histedit) tends to be a lot clearer to Mercurial newbies, than Git's rebase is to Git newbies. (This is a general theme in Mercurial-vs-Git.)

In Git, commits are never actually tied to any particular branch. Instead, Git finds commits by starting from some branch name—which is really much more like a Mercurial "bookmark"—and then working backwards. The set of commits reached by this working-backwards part is said to be "on the branch".

As a result, when two or more branch names identify the same final commit—as if you had two or more bookmarks that point to the same commit in Mercurial—those commits are on the same branch, but as soon as you move one or both of those branch names in Git, those same two commits, totally unchanged, may not be on those same branches.

The git rebase command works by:

  1. Saving the current branch name somewhere.
  2. Enumerating the raw hash IDs of commits to be copied.
  3. Using Git's detached HEAD mode to select some particular target (--onto) commit. This is where the new copies will go.
  4. Copying the commits, one at a time, as if by git cherry-pick (equivalent to Mercurial hg graft except that branch names don't matter: commits are never tied to any branch).
  5. Last, yanking the branch name saved in step 1 to point to the last copied commit, and exiting "detached HEAD" mode to be back on the branch you were on in step 1.

To handle steps 2 and 3—the "which commits to copy" list, and the "target" commit—Git typically use a single argument. That single argument is typically a target branch name. The source of the commits-to-be-copied is thus the current branch (per step 1). That's why we start with:

git switch main

or equivalent.

To list out the commits to be copied, Git then uses the rough equivalent of:

git log target..HEAD

which means find commits reachable from HEAD but not from target. (Mercurial has this as well, using target::., except that Mercurial's :: graph operator includes both ends of the interval, while Git's is a half-open interval: it always excludes the leftmost commit.)

The detached-HEAD-checkout target is literally the target argument in this form.

In some cases, it's not possible to generate the correct list of commits this way, so git rebase has the --onto syntax, which is kind of weird:

git rebase --onto target upstream

The list of commits in step 2 is now upstream..HEAD instead of target..HEAD. The checkout in step 3 is still target.

Given:

CCCCC (HEAD -> main) foobar issue 666
| * BBBBB (tag: fubar, origin/main, origin/HEAD) fubar issue #69
|/  
* AAAAA foo

the list of commit hash IDs that you'd like copied is just one element long, listing CCCCC. The place you want the copies to go after is commit BBBBB. So if needed—the HEAD -> main says it isn't—we git checkout main or git switch main to make commit CCCCC current (it's OK to do even if not needed, it's just a no-op then). Then we run git rebase origin/main.

Git now lists all the hash IDs from here (HEAD or CCCCC) backwards: that's CCCCC, then AAAAA, then anything under there. From this list, Git strips BBBBB (it's not there but that just makes the stripping-out go fast!), then AAAAA, then anything under there—leaving only CCCCC.

Now Git does a detached-HEAD checkout of BBBBB. It then issues cherry-picks for all the commits in the saved list, in the right order. There's just the one commit in that list: CCCCC. So Git makes a new commit, CCCCD perhaps, that's like CCCCC, doing the same sort of thing, but that comes after BBBBB:

CCCCC (main) foobar issue 666
| * CCCCD (HEAD) foobar issue 666
| * BBBBB (tag: fubar, origin/main, origin/HEAD) fubar issue #69
|/  
* AAAAA foo

Now that all the copies are done, Git yanks the name main to here—to CCCCD—and re-attaches HEAD so that we have:

CCCCC foobar issue 666
| * CCCCD (HEAD -> main) foobar issue 666
| * BBBBB (tag: fubar, origin/main, origin/HEAD) fubar issue #69
|/  
* AAAAA foo

Commit CCCCC can't be found, though, so git log doesn't show it:

* CCCCD (HEAD -> main) foobar issue 666
* BBBBB (tag: fubar, origin/main, origin/HEAD) fubar issue #69
  
* AAAAA foo

and there's no longer any reason for the blank line either, so that goes away.

  • Related