views:

270

answers:

2

I use git to keep track of changes made by our development team and committed into our central cvs-style repository. Since its cvs, it keeps track of files and not commits, making it sometimes difficult to tell exactly what files constitute the full patch for a bug fix. I just came across one and did the following:

1) trolling along, checking CVS logs and committing them to git as full patches

A--B--C--D

2) Found another file change that was actually for ticket (B), so I reset the current branch to B with

git reset --soft <sha1 ID for commit B>

3) I copy in the change, and append it to commit (B) with

git commit --amend

4) to my surprise, the tree now reads

A--B

with commits (C) and (D) only in the working tree. Their details are gone from the logs and I don't think I can get them back. Where did I go wrong? Is my only option to make an additional commit on top of (D), and just know that its really part of (B)?

+7  A: 

What happened

You mean amend, not append, right? I'm going to pretend this was on a branch called master, for convenience. This is what your repository looks like now:

A---B' (master)
 \
  \-B---C---D

Git commits explicitly depend on their parents - the same patch on top of a different parent is a different commit.

How to recover

You can recover the previous position of a few ways. There's a nice shorthand for previous positions, which you can use to directly check it out or create a branch:

git checkout master@{1}
git branch oldmaster master@{1}

This is assuming it's the first previous position. It might be the second (master@{2})... or if you know when it was, you can use master@{7:35} or master@{23.hours.ago}. For a summary of these forms, see the "specifying revisions" section of man git-rev-parse (online here).

If you're not sure exactly how to get to it, try

git reflog show master

This will give you a list of previous positions of master, and you should be able to tell from the descriptions which one you want (or maybe try a few). You can simply copy hashes from the list, and use git checkout or git branch as above.

What you should have done

Warning: editing history is a bad idea if it's been published already - in that case, you should simply commit the fix. Yes, it's kind of ugly having it split into two commits in the repository, but other users have to be able to trust what they've seen in the public repo not to change!

That said, to do this particular kind of history editing, you want interactive rebase:

git rebase -i master~4 master

master~4 represents the commit four commits before the tip of master. You can use any form you want here - maybe it's another branch, maybe a commit hash - whatever works.

This will open up in an editor a list of the commits you're playing with:

pick <hash-A>  <message-A>
pick <hash-B>  <message-B>
pick <hash-C>  <message-C>
pick <hash-D>  <message-D>

# Rebase <hash-A^>..<hash-D> onto <hash-A^>
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

The commented-out help text there is pretty self-explanatory. In this case, you want to change 'pick' to 'edit' on commit B's line, save, and quit. The rebase will start, and it'll pause after applying B to let you make changes. You'll do what you need to, add, use git commit --amend, and then git rebase --continue. It'll apply C and D, and you'll be done. If anything goes wrong in the middle, use git rebase --abort to get back to where you started.

Rebasing can be kind of scary - for example, don't accidentally remove lines in that list! If you're not comfortable with it yet, it's a good idea to make heavy use of gitk and backup branch names.

Jefromi
This is kind of an incredible answer! Awesome.
Dan Fitch
Brilliant! I had a feeling it had something to do with git-rebase. Someone buy that man a beer.
David Dombrowsky
*Great* answer. Excellent job of illustrating the power of git, too. This is the kind of stuff SVN die-hards need to see.
Dan Moulding
+1  A: 
Jakub Narębski