How can I revert multiple Git commits?

Viewed 895426

I have a Git repository that looks like this:

A <- B <- C <- D <- HEAD

I want the head of the branch to point to A, i.e., I want B, C, D, and HEAD to disappear and I want head to be synonymous with A.

It sounds like I can either try to rebase (doesn't apply, since I've pushed changes in between), or revert. But how do I revert multiple commits? Do I revert one at a time? Is the order important?

17 Answers

Using Git Restore

You can also use the restore command:

A <- B <- C <- D <- HEAD

Let's say you want HEAD to look exactly like A. Make sure you've pulled the latest master. Then cut a new branch.

git switch -c feature/flux-capacitor  # synonymous with checkout -b
git restore --source A .
git add .
git commit
git push

The restore command changes everything (.) to what it was at the --source commit. Then you commit that to your local branch and push it to the origin. You can then open a PR against it.

This has the benefit of not changing any history that others might have based work on. It also leaves a useful history for folks in the future.

Docs: git restore

In my opinion a very easy and clean way could be:

go back to A

git checkout -f A

point master's head to the current state

git symbolic-ref HEAD refs/heads/master

save

git commit

Probably less elegant than other approaches here, but I've always used get reset --hard HEAD~N to undo multiple commits, where N is the number of commits you want to go back.

Or, if unsure of the exact number of commits, just running git reset --hard HEAD^ (which goes back one commit) multiple times until you reach the desired state.

I found myself needing to revert a long range of commits and then re-revert them to help a team present a clear pull request without having to force-push over their target branch (which was committed directly to)

# checkout the branch that should be targeted
git checkout $branch_target

# revert the commits in $branch_target to some $count where
#   $count is the number of commits to revert
#   cut is used to slice just the commit hash field from each line of output
#   xargs runs the command once for each line of input, reversing the commits!
git log --oneline -n $count | cut -d' ' -f1 | xargs git revert

# check out the branch which should be the source of the pull request
git checkout -b $branch_for_pull

# revert the revert commits
# $count is that same number of commits being reverted (again)
git log --oneline -n $count | cut -d' ' -f1 | xargs git revert

# push branches up and go off to create PR in whatever web UI
git push --set-upstream origin $branch_for_pull  # it's new!
git checkout $branch_target
git push  # if this branch wasn't pushed, just fix the issue locally instead..

Because this reverts all of the commits from HEAD to git log -n $count in reverse order, it'll work well and cleanly with any number of commits

View from $branch_target at this state

% git log --oneline origin/$branch_target
ffff006 (origin/$branch_target, $branch_target) Revert "first commit"
ffff005 Revert "second commit"
ffff004 Revert "third commit"
ffff003 third commit
ffff002 second commit
ffff001 first commit

View from $branch_for_pull at this state

% git log --oneline origin/$branch_for_pull
ffff009 (origin/$branch_for_pull, $branch_for_pull) Revert "Revert "third commit""
ffff008 Revert "Revert "second commit""
ffff007 Revert "Revert "first commit""
ffff006 (origin/$branch_target, $branch_target) Revert "first commit"
ffff005 Revert "second commit"
ffff004 Revert "third commit"
ffff003 third commit
ffff002 second commit
ffff001 first commit

If the intention was to create N branches with changesets, but they were all committed to the same branch, you can still revert all of them back to the base commit, then only revert the reverts needed as the changesets should be ordered logically (try saying that 5x fast)

Using a syntax like HEAD~7..HEAD~5 may help with describing the ranges to precisely split the revert-revert branches

Here, it would make sense when reverting the last 7 commits (git log -n 7), but restoring 5 with in one branch (git log -n 5) and 2 then the top-most 2 in another git log HEAD~12..HEAD~10 (12 is 7 commits + 5 commits, assuming the new PR branch is based off either the branch "before" it, or the result of a FF (non-squashed) merge the branch "before" it into the original target branch)

I really wanted to avoid hard resets, this is what I came up with.

A -> B -> C -> D -> HEAD

To go back to A (which is 4 steps back):

git pull                  # Get latest changes
git reset --soft HEAD~4   # Set back 4 steps
git stash                 # Stash the reset
git pull                  # Go back to head
git stash pop             # Pop the reset 
git commit -m "Revert"    # Commit the changes

If you

  1. have a merged commit and
  2. you are not able to revert, and
  3. you don't mind squashing the history you are to revert,

then you can

git reset --soft HEAD~(number of commits you'd like to revert)
git commit -m "The stuff you didn't like."
git log
# copy the hash of your last commit
git revert <hash of your last (squashed) commit>

Then when you want to push your changes remember to use the -f flag because you modified the history

git push <your fork> <your branch> -f
Related