Skip to content

Instantly share code, notes, and snippets.

@GeorgeLyon
Last active September 25, 2019 18:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GeorgeLyon/2a0f53f93db00a4e597e25ae276b4039 to your computer and use it in GitHub Desktop.
Save GeorgeLyon/2a0f53f93db00a4e597e25ae276b4039 to your computer and use it in GitHub Desktop.
A simpler mechanism for rebasing a chain of commits

Rebasing a chain of commits

Disclaimer

The following uses some complex git commands, so it should be undertaken with caution. That said, the mechanisms involved are fairly straightforward so someone reasonably familiar with git should be able to follow along. While it is possible to mess up your local checkout, nothing below is destructive, and you should be able to reference all your original commits by hash were something to go wrong.

Process

Let's say you are working on a big feature with changes A, B and C, which you want to merge incrementally. Initially your commit graph would look something like this:

* 8315ba3 - (HEAD -> branch-c) C²
* 071487f - C¹
* 98e4743 - (branch-b) B²
* c5330d2 - B¹
* a32de0d - (branch-a) A²
* 87b33b6 - A¹
* 79a0503 - (master) O¹

By the time A is reviewed, master has progressed and you need to rebase:

* 597962f - (HEAD -> master) O²
| * 8315ba3 - (branch-c) C²
| * 071487f - C¹
| * 98e4743 - (branch-b) B²
| * c5330d2 - B¹
| * a32de0d - (branch-a) A²
| * 87b33b6 - A¹
|/  
* 79a0503 - O¹

I have always done this manually, which is a little painful, but there is a better way!

  1. Graft the base commit of your changes (In this case, ) onto the commit you want to rebase off of: git replace --graft 87b33b6 master
  2. Filter the branches you want to move over: git filter-branch master..branch-{a,b,c}. It is very likely you want to filter all the branches that are descendants of the base commit (in this case, ). You can achieve this with git filter-branch $(git branch --contains 87b33b6 | xargs -I {} echo 87b33b6~1..{}) where 87b33b6 is the SHA of your base commit (which occurs twice in the example).
  3. Force push the branches you are interest in, the same way you would if you had just done git rebase master A similar approach can be used if someone requests changes to and you want to keep your branch up to date

Explanation

git replace is a funny little mechanism built into the core of git. It places a file in $GIT_DIR/refs/replace which states that "this commit is now this other commit". You can see the effect of this by looking at the commit graph after the git replace --graft operation (notice "replaced" in the log).

~/D/P/git-test> git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s%Creset' --abbrev-commit master branch-{a,b,c}
* 8315ba3 - (branch-c) C²
* 071487f - C¹
* 98e4743 - (branch-b) B²
* c5330d2 - B¹
* a32de0d - (branch-a) A²
* 87b33b6 - (replaced) A¹
* 597962f - (HEAD -> master) O²
* 79a0503 - O¹

git filter-branch is the Thor's hammer of git tools, but here we are just using it to drive in a few nails. Without filters, it rewrites the references you pass it without changing them (and because of the master.. prefix, it only affects the specified range). The catch here is that when it rewrites the replaced commit, it creates a real commit, and all descendent commits are updated accordingly.

Detailed step-by-step

~/D/Projects> mkdir git-test
~/D/Projects> cd git-test/
~/D/P/git-test> git init
Initialized empty Git repository in /Volumes/Shared/Developer/Projects/git-test/.git/
~/D/P/git-test> git commit --allow-empty -m "O¹"
[master (root-commit) 79a0503] O¹
~/D/P/git-test> git checkout -b branch-a
Switched to a new branch 'branch-a'
~/D/P/git-test> git commit --allow-empty -m "A¹"
[branch-a 87b33b6] A¹
~/D/P/git-test> git commit --allow-empty -m "A²"
[branch-a a32de0d] A²
~/D/P/git-test> git checkout -b branch-b
Switched to a new branch 'branch-b'
~/D/P/git-test> git commit --allow-empty -m "B¹"
[branch-b c5330d2] B¹
~/D/P/git-test> git commit --allow-empty -m "B²"
[branch-b 98e4743] B²
~/D/P/git-test> git checkout -b branch-c
Switched to a new branch 'branch-c'
~/D/P/git-test> git commit --allow-empty -m "C¹"
[branch-c 071487f] C¹
~/D/P/git-test> git commit --allow-empty -m "C²"
[branch-c 8315ba3] C²
~/D/P/git-test> git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s%Creset' --abbrev-commit master branch-{a,b,c}
* 8315ba3 - (HEAD -> branch-c) C²
* 071487f - C¹
* 98e4743 - (branch-b) B²
* c5330d2 - B¹
* a32de0d - (branch-a) A²
* 87b33b6 - A¹
* 79a0503 - (master) O¹
~/D/P/git-test> git checkout master
Switched to branch 'master'
~/D/P/git-test> git commit --allow-empty -m "O²"
[master 597962f] O²
~/D/P/git-test> git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s%Creset' --abbrev-commit master branch-{a,b,c}
* 597962f - (HEAD -> master) O²
| * 8315ba3 - (branch-c) C²
| * 071487f - C¹
| * 98e4743 - (branch-b) B²
| * c5330d2 - B¹
| * a32de0d - (branch-a) A²
| * 87b33b6 - A¹
|/  
* 79a0503 - O¹
~/D/P/git-test> git replace --graft 87b33b6 master
~/D/P/git-test> git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s%Creset' --abbrev-commit master branch-{a,b,c}
* 8315ba3 - (branch-c) C²
* 071487f - C¹
* 98e4743 - (branch-b) B²
* c5330d2 - B¹
* a32de0d - (branch-a) A²
* 87b33b6 - (replaced) A¹
* 597962f - (HEAD -> master) O²
* 79a0503 - O¹
~/D/P/git-test> git filter-branch master..branch-{a,b,c}
Rewrite 87b33b630346a0e13bb5f9ea3bbebc421b361a13 (1/6) (0 seconds passed, remaining 0 predicted)
Rewrite a32de0d49d28b02383be594fe41d5e3ff2529a3e (2/6) (0 seconds passed, remaining 0 predicted)
Rewrite c5330d26881740a4226ccc08613498802b2a43d6 (3/6) (0 seconds passed, remaining 0 predicted)
Rewrite 98e474324b4089cdb770ee3b2f46fb03ab7814a1 (4/6) (0 seconds passed, remaining 0 predicted)
Rewrite 071487fc8e64f90fb1ba9fc831df182300c77d45 (5/6) (0 seconds passed, remaining 0 predicted)
Rewrite 8315ba39cde3a9717c8b1129fa24923546a0e5eb (6/6) (0 seconds passed, remaining 0 predicted)

Ref 'refs/heads/branch-a' was rewritten
Ref 'refs/heads/branch-b' was rewritten
Ref 'refs/heads/branch-c' was rewritten
~/D/P/git-test> git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s%Creset' --abbrev-commit master branch-{a,b,c}
* 863e5cd - (branch-c) C²
* 986cd49 - C¹
* 28a3521 - (branch-b) B²
* 683489d - B¹
* 9ee9182 - (branch-a) A²
* 52e8527 - A¹
* 597962f - (HEAD -> master) O²
* 79a0503 - O¹
~/D/P/git-test> 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment