Undo change in git (not rewriting history)

Yes, you can use git revert for this. See the git manual section on this for more information.

The gist is that you can say:

git revert 4f4k2a

Where 4f4k2a is the id of the commit you'd like to undo, and it will try to undo it.


Just a comment:

git revert aCommit

does revert the all commit (as in "all the files part of the commit" ):
it computes a reverse patch, applies it on HEAD and commit.

So two problems here (the first one is easily solved):

  • it does always commit, so you may want to add -no-commit option: "git revert --no-commit aCommit": this is useful when reverting more than one commits' effect to your index in a row.
  • it does not apply for a specific file (what if you a.py was part of a commit with 1000 other changes that you may not want to revert) ?
    For that, if you want to extract specific files as they were in another commit, you should see git-checkout, specifically the git checkout <commit> <filename> syntax (that is not exactly what you need in this case though)

Easy Git (Elijah Newren) tried to bring a more "complete revert" to the Git Mailing list; but without much success:

People occasionally want to "revert changes".

Now, this may be:

  • the changes between 32 and 29 revisions ago,
  • it might be all changes since the last commit,
  • it could be the changes since 3 commits ago, or
  • it could be just one specific commit.
  • The user may want to subset such reversions to just specific files,

(eg revert is documented here, but I am not sure it is part of the current distribution of eg though)

but it all boils down to "reverting changes" in the end.

eg revert --since HEAD~3  # Undo all changes since HEAD~3
eg revert --in HEAD~8     # much like git revert HEAD~8, but nocommit by default
eg revert --since HEAD foo.py  # Undo changes to foo.py since last commit
eg revert foo.py               # Same as above
eg revert --in trial~7 bar.c baz.  # Undo changes made in trial~7 to bar.[ch]

Are these kinds of "reverting data" really so different that there should need to be different commands, or that some of these operations shouldn't be supported by the simple revert command?
Sure, most users most of the time will probably use the "eg revert FILE1 FILE2..." form, but I didn't see the harm in supporting the extra capabilities.

Also...is there anything fundamental that would keep core git from adopting such behavior?

Elijah

Note: commits by default don't make sense for the generalized revert command, and "git revert REVISION" would error out with instructions (telling the user to add the --in flag).


Lets say you have, out of 50 committed, 20 files you realize that old commit X introduced changes that should not have taken place.
A little plumbing is in order.
What you need is a way to list all the specific files you need to revert
(as in "to cancel changes made in commit X while keeping all subsequent changes"),
and then, for each of them:

git-merge-file -p a.py X X^

The issue here is to recover the lost function without obliterating all subsequent changes in a.py you might want to keep.
That technique is sometime called "negative merging".

Since git merge-file <current-file> <base-file> <other-file> means:
incorporates all changes that lead from the <base-file> to <other-file> into <current-file>, you can restore the deleted function by saying you want to incorporate all changes.)

  • from: X (where the function has been deleted)
  • to: X^ (the previous commit before X, where the function was still there)

Note: the '-p' argument which allows you to review first the changes without doing anything on the current file. When you are sure, remove that option.

Note: the git merge-file is not that simple: you can not reference previous versions of the file just like that.
(you would have over and over the frustrating message: error: Could not stat X)
You have to:

git cat-file blob a.py > tmp/ori # current file before any modification
git cat-file blob HEAD~2:a.py > tmp/X # file with the function deleted
git cat-file blob HEAD~3:a.py > tmp/F # file with the function which was still there

git merge-file a.py tmp/X tmp/F # basically a RCS-style merge
                                 # note the inversed commit order: X as based, then F
                                 # that is why is is a "negative merge"
diff -u a.py tmp/ori # eyeball the merge result
git add a.py 
git commit -m "function restored" # and any other changes made from X are preserved!

If this is to be done for a large number of files within a previous commit... some scripting is in order ;)


To revert the changes to only one file within a commit, as VonC pointed out, I would checkout the branch (master or trunk or whatever) and then checkout the version of the file I wanted to revert and treat it as a new commit:

$ git checkout trunk
$ git checkout 4f4k2a^ a.py
$ git add a.py
$ git diff              #verify I'm only changing what I want; edit as needed
$ git commit -m 'recover function deleted from a.py in 4f4k2a'

There's probably a plumbing command that would do this directly, but I wouldn't use it if I knew it. It's not that I don't trust Git, but I don't trust myself -- I wouldn't trust that I knew without looking what was changed in that file in that commit and since then. And once I look, it's easier to just build a new commit by editing the diff. Perhaps that's just personal workstyle.


Have a look at this git revert question. There seems to be a issue reverting older commits if not in a consecutive sequence including the most recent commit.