What you have now is a sequence of commits on some branch:
...--o--*--B1--B2--B3 <-- branch
where *
is a good commit, but B1
is a "bad" commit (has files removed that you decided, now, you don't want to remove after all).
You'll need to re-do your bad commit(s) as good commit(s)—and also, to copy any good commits that come after a bad commit. In other words, you will basically repeat the following three steps as needed:
Copy the commit you don't like to a new commit (but don't actually commit it just yet). Starting from a new branch that will grow from the commit marked *
, use git cherry-pick -n
to copy a bad commit like B1
, but not actually commit it.
Then, restore the missing file(s): git checkout -- <path>
. (You can also do this with git reset
, but when we get to using git rebase -i
below, it gets too hard, so we'll stick with git checkout
.)
Finally, make the new commit that is a copy of the old commit: git commit
. Since this is a cherry pick, Git will have the old commit's message ready to be edited.
Now the first bad commit B1
has been copied to a new, corrected, good commit G1
:
...--o--*--B1--B2--B3 <-- branch
\
G1 <-- new branch [new copies being made]
Now you can copy the second commit, B2
. If it has a deletion you don't want, use the same three-part sequence (cherry-pick -n
, checkout
, commit
). If it's fine, leave out the -n
to just copy it and commit:
...--o--*--B1--B2--B3 <-- branch
\
G1--G2 <-- new branch [new copies being made]
And now you can copy the third commit, B3
. As before, if it has something you need to change, you can fix it up along the way. (This assumes there are three commits in the B1--...--Bn
chain; if there are more, or fewer, adjust as needed.)
When everything is all done, you will need to convince Git to peel the branch
label off the old branch and paste it on the new one instead. There is a way to do that, but...
git rebase -i
There's a much easier way to do it. The git rebase
command works by doing a series of cherry-picks.1 When the cherry-picks are all done, the git rebase
command peels off the old branch label, and pastes the label on the new branch it just built. This is exactly what you need to do, so all you have to do is convince git rebase
to let you tell it which commits are good commits, and which are bad ones, and to pause at bad ones.
This is where git rebase -i
comes in. It brings up your editor on a set of instructions. Initially, all the instructions are pick
: do a cherry pick of each commit. You can change any one instruction to edit
, which tells Git to do that particular cherry-pick, but then stop and let you fix / change things.
Slightly annoyingly, rebase
does the cherry-pick without -n
: it makes the copy and commits it. So you have to tweak step 2 a bit: instead of git checkout -- <path>
to get the file back from HEAD
, you need to git checkout HEAD^ -- <path>
or git checkout HEAD~1 -- <path>
. This means "reach back into the previous commit, and get that version of the file." Having done that, you can then run git commit --amend
to update the commit.
Then, continue the rebase process with git rebase --continue
. The rebase code will go on through more pick
s to the next edit
, or if there are only pick
s remaining, finish them all off and finish the rebase and move the branch label.
The main thing is to identify commit *
: the last "good" commit. Spelling out commit *
in some way lets you do the git rebase -i
. In our example, it's three steps back from the current branch tip, so:
git rebase -i HEAD~3
will do the trick. If it's more or fewer steps back, you'll need to adjust the number after the tilde ~
character.
Note that whatever you do, you will get copies of the original commits. Also, git rebase
normally removes merge commits when making copies. Both of these mean you must be cautious if you've made these commits available to anyone else (by git push
or similar), or if you have merges following the first bad commit you intend to "replace" (really, copy and then ignore the originals).
1In fact, git rebase
literally runs git cherry-pick
sometimes. Other times, it uses something else that's pretty much the same anyway.