tags:

views:

2866

answers:

3

I made a change in a script and committed it. Then I made a few other changes, and pushed them to a remote repository and such.

Then I realised that first change I mentioned was stupid, and want to undo it.. Can I "unapply" that commit, without manually copy/pasting the diff?

As an example: I have two files, a.py and b.py:

Commit 1:
I delete a function in a.py

Commit 2:
I change a few lines in b.py

Commit 3:
I change the docstring in a.py

Can I undo that function deletion, and make it appear as "commit 4" (rather than deleting commit 1)

+15  A: 

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.

Jesse Rusak
Aha, of course, thanks!
dbr
BTW! It will delete that commit 1 object
aatifh
Hm, it didn't seem to delete the revert'd commit..
dbr
I don't think you can delete the commit from history, but you can undo the change.
Jeremy French
I suppose git is so advanced users would forgot something as simple as a revert... lol :-P
chakrit
+11  A: 

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 ;)

VonC
I don't think you need the dash in: `git checkout <commit> <filename>`
Paul
+1  A: 

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.

Paul
Two comments: 1/if the function has been deleted in the 4f4k2a commit for the a.py, shouldn't you checkout a.py from the *previous* commit ? (the one *before* 4f4k2a, as in 4f4k2a^ ?) 2/ Would that not erase completly the current version of a.py ? What if I want to keep subsequent changes ?
VonC
I would rather make a "negative merge" with git-merge-file : see my completed answer
VonC
1) Yes. Thanks for fixing the other typo.2) Yes -- That's why I'd have to see the diff. I'd actually use an hacked diffmerge in 3-way merge mode, which shows me a more useful diff that I'm used to working with, but git diff is usable. The point is: I'm only confident if I've looked at the code.
Paul
3) Yes, I think 'negative merge' would work. But to me it takes too much time to research, when I know I can read the code and make the correcting commit quickly. I think I've used git-merge-file in two other SO answers where nothing else would work, but it's not really in my day-to-day usage.
Paul
Just added the git merge-file use case in my answer (see the end)... that command is not easy to use: you cannot just reference a previous version of a file, you have to cat into a temporary file. That is a disappointment...
VonC
That's messier than I expected, though now that you've detailed it out, I see why it's necessary. A nice piece of work figuring it out.I suggest you drop the EasyGit section. It's good context but I'm aafraid it distracts from the meat of your answer.
Paul