tags:

views:

28

answers:

1

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 ?

+1  A: 

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.

rafl
Thanks rafl for your answer. While this works fine on the clone, I fail to push changes to the origin master. I sorry for not mentioning that I have to push those changes to a remote repository, sorry about that. I edited my question to be more clear. Thanks !
leucos
Pushing that would be a non-fast-forward as you have rewritten published history. You can either force a push, or do it differently, as i'm going to point out in my answer in a second. You should probably read up on rewriting history in general and on what non-fast-forwards are.
rafl
Thanks rafl, this does the trick. reverting both commits, editing fileA along solves does what I was looking for. I need to read for sure !
leucos