Search code examples
gitgit-revert

Is there a way to 'soft revert' an old commit?


Is there a way to revert an old commit without changing the files in the working directory? I made a commit a while back, but the code that I committed wasn't ready and I want those changes to stay in my working directory as uncommitted changes so that I can continue working on them now. I looked into the revert command but that changes the working directory. I was thinking about checkout the old revision, resetting the head 1 and stashing the changes, but I already have the changes. I just want it to be as if I never made that one commit way back then, but I want the changes to the files to stay.


Solution

  • The short answer is "no".

    The longer answer: git revert—like all merge-oriented operations, including git cherry-pick, git rebase, and of course git merge itself—modifies a modifiable work-tree, using an index. You can commit or stash your current work, or get a second work-tree (with another clone, or using git worktree if you have Git version 2.5 or later).

    You can use git revert -n, which allows the work-tree to be dirty, but I would not really advise that here.

    The way I would handle this is to make a commit now:

    $ git add ...
    $ git commit -m 'temporary commit, do not push'
    

    Then, do the desired revert as a regular commit:

    $ git revert <hash>
    

    Then use git rebase -i to swap the order of the new revert commit and the temporary commit:

    $ git rebase -i HEAD~2  # and edit the two "pick" lines
    

    then use git reset --soft HEAD^ (or HEAD~1, same thing) or git reset --mixed HEAD^ to un-commit the temporary commit. At this point you are in the same state (mostly) as you were before you made the temporary commit.

    The (mostly) has to do with the index state: if you use git reset --soft, all those git adds are in effect now. If you use git reset --mixed, none of those git adds are in effect now.

    For experts only

    If, before you start any of this, you already have some carefully staged state—for instance, from git add -p—that you want to preserve, this requires doing two commits, and git stash before the revert, with git stash apply --index afterward, is simpler. This is because git stash actually makes two commits, and git stash apply --index extracts and applies those two commits, separately, to the index and work-tree. (Then git stash drop the stash if all has gone well.)

    Depending on how comfortable you are with git stash, you can even replace the whole sequence above with git stash && git revert <hash> && git stash pop to do it as a one-liner. Add --index to recover separate index state. (Note that if you typo --index and your Git version is not modern enough to detect the mistake, this will mash together the two separate commits and drop the stash. This is why I like to use git stash apply instead of git stash pop here.)

    If anything goes wrong during this process, you will need to know exactly what happened, and what to do about it, hence the "for experts only" header above this part.