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.
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!
- Graft the base commit of your changes (In this case,
A¹
) onto the commit you want to rebase off of:git replace --graft 87b33b6 master
- 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,A¹
). You can achieve this withgit filter-branch $(git branch --contains 87b33b6 | xargs -I {} echo 87b33b6~1..{})
where87b33b6
is the SHA of your base commit (which occurs twice in the example). - 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 toA¹
and you want to keep your branch up to date
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.
~/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>