Hi there,
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 ?