Pruning several consecutive commits in history
Is there anyway to remove several consecutive commits in a branch ?
Let's says the history looks like this :
A -> B -> C -> D
Now, I would like to remove changes introduced by B and C, so the history looks like :
A -> D
For instance, a typical scenario would be a drunk developer that committed trash in commits B and C, while writing good stuff at D.
I came up with a rather poor (and probably not very robust way to do it) :
# Get a patch for the good things
# Context lines are set to zero so applying
# the patch won't choke because of missing lines
# that were added by C or D
git format-patch --stdout -U0 revC..revD > CtoD
# Go back in time to last good revision before mayhem
git reset --hard revA
# Apply good things at this point
git apply --stat CtoD
git apply --check CtoD
git apply CtoD
# Add new files from patch
git add <any files that were created in CtoD patch>
# And commit
git commit -a -m "Removed B and C commits. Drunk dev fired"
This way of doing it is far from perfect. Removing context for the diff will probably make git-apply choke on many situations, and files must be git-add'ed by hand. I also might miss the point completely here...
Can someone point me to the right way to do this ?
Thanks for reading !
EDIT :
I forgot to say that I have to push all this stuff to a remote repository, so I tried rafl proposal and while it's ok on the clone, it is not possible to push the stuff to the origin properly.
Here is a detailed (and long, sorry !) list of what have been done :
##
# Create test environment
##
# all will happen below 'testing', so the mess can easily be wiped out
mkdir testing
cd testing
# Create 'origin' (sandbox_project) and the working clone (work)
mkdir sandbox_project work
# Create origin repos
cd sandbox_project
git init --bare
# Clone it
cd ../work
git clone ../sandbox_project
cd sandbox_project
# Create a few files :
# at each rev (A, B, C, D) we respectively create a fileA .. fileD
# at each rev, we also add a line to fileA
echo "This file was created in A" > fileA
git add .
git commit -a -m 'First revision'
git tag "revA"
for i in B C D; do
echo "This file was created in $i" >> file$i
echo "This change was done in $i" >> fileA
git add file$i
git commit -a -m "revision $i"
git tag "rev$i"
done
# We push changes to origin
git push origin master
Now, it looks like this :
开发者_如何学运维$ git log --graph --decorate --pretty=oneline --abbrev-commit
* e3dc9b7 (HEAD, revD, origin/master, master) revision D
* e21fd6a (revC) revision C
* a9192ec (revB) revision B
* a16c9dd (revA) First revision
I want to remove what has been introduced in B and C :
$ git rebase -i HEAD~3
Automatic cherry-pick failed. After resolving the conflicts,
mark the corrected paths with 'git add <paths>', and
run 'git rebase --continue'
Could not apply e3dc9b7... revision D
$ git status
# Not currently on any branch.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: fileD
#
# Unmerged paths:
# (use "git reset HEAD <file>..." to unstage)
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: fileA
#
Of course, there is a problem with fileA (problem I solved with my solution by applying a diff generated with -U0. Here, I edit fileA, remove conflicting lines, and continue rebase :
This file was created in A
<<<<<<< HEAD
=======
This change was done in B
This change was done in C
This change was done in D
>>>>>>> e3dc9b7... revision D
is edited to be :
This file was created in A
This change was done in D
And then :
$ git add fileA
$ git rebase --continue
# leave mesasge as-is
[detached HEAD e2d4032] revision D
2 files changed, 2 insertions(+), 0 deletions(-)
create mode 100644 fileD
Successfully rebased and updated refs/heads/master.
$ git push
To ../sandbox_project
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to '../sandbox_project'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes before pushing again. See the 'Note about
fast-forwards' section of 'git push --help' for details.
Well, this looks normal to me, since I rebased, so I tried :
$ git pull
Auto-merging fileA
CONFLICT (content): Merge conflict in fileA
Automatic merge failed; fix conflicts and then commit the result.
$ git log --graph --decorate --pretty=oneline --abbrev-commit
* e2d4032 (HEAD, master) revision D
* a16c9dd (revA) First revision
Again, fileA can not be merged since it has revisions B and C cruft. Here we go again, editing fileA, and removing changes introduces by and C.
Then :
$ git add fileA
$ git commit -a
[master b592261] Merge branch 'master' of ../sandbox_project
$ git push
Counting objects: 9, done.
Compressing objects: 100% (5/5), done.
Unpacking objects: 100% (5/5), done.
Writing objects: 100% (5/5), 674 bytes, done.
Total 5 (delta 0), reused 0 (delta 0)
To ../sandbox_project
e3dc9b7..b592261 master -> master
Looks fine, but :
$ git log --graph --decorate --pretty=oneline --abbrev-commit
* b592261 (HEAD, origin/master, master) Merge branch 'master' of ../sandbox_project
|\
| * e3dc9b7 (revD) revision D
| * e21fd6a (revC) revision C
| * a9192ec (revB) revision B
* | e2d4032 revision D
|/
* a16c9dd (revA) First revision
While in the end, fileA is ok, I still have fileB and fileC I didn't want. And in the course of things, I had to resolve merging conflicts for fileA twice.
Any clue ?
I'd use git rebase -i HEAD~4
.
That'll spawn your $EDITOR
with a file containing one line for each commit between HEAD~4
and HEAD
. Delete the lines for the commits you want to throw away, save and exit your editor, and git will apply only the commits you left in the file on top of HEAD~4
.
However, as this actually rewrites your history, it's often not possible to push the result of that anywhere without screwing all the other pople that also work off of that repository.
As an alternative, you could do what you'd do with pretty much every other version control system as well: revert the bad commits.
$ git revert $commit_sha
That'll create a new commit, applying the diff of $commit_sha
in reverse.
精彩评论