Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reverting a series of pushed merges and commits in Git (without rewriting history)

Tags:

git

git-revert

Context

One of my teammates mistakenly pushed some commits to our main development branch. We're a small, collocated team. Our remote repository is hosted on an internal server.

Here is the top of our commit log (all these commits have already been pushed):

$ git log develop -6 --pretty=oneline --abbrev-commit faada93 Merge branch 'develop' of <our_repo_path>.git 244d174 Support classes again a97a877 Pruned all unused references (again). 8c29252 Merge branch 'develop' of <our_repo_path>.git a78b993 Support models & methods - product types & categories da8b496 Resolved JIRA issue PPF-182 

da8b496 is the last commit we wanted to keep in our develop branch, so we needed to revert the 5 last commits. We created a new branch from 8c29252 to continue work in a "feature branch."

I tried many things, guided by this answer and this post from Linus, and ended up doing what you can see in my Terminal history below. But I'm not sure if what I ended up doing is "the right way." The information I found was complex; I was unable to discern a "best solution" for this particular problem.

Question

Was the approach I chose (see details below) a good way to revert those 5 commits, without harming our history? Is there an easier or "more correct" way to accomplish the same thing?

Amongst other things, I considered creating a new branch from da8b496 (git checkout -b new-develop da8b496) and abandoning our current develop branch, but that just didn't feel right.


What I ended up doing (details)

First, I created a new branch for the commits a78b993 and 8c29252, because these commits contain work that we want to keep and eventually merge back to our main development branch.

$ git checkout -b new-feature-brach 8c29252 

Then I started reverting the offending commits in our development branch.

I tried this first, but it didn't work (likely because some of the commits are merges):

$ git revert a78b993..HEAD error: a cherry-pick or revert is already in progress hint: try "git cherry-pick (--continue | --quit | --abort)" fatal: revert failed 

So… I manually reverted each commit instead; one by one:

$ git revert -m 1 faada93 [develop 40965a5] Revert "Merge branch 'develop' of <our_repo_path>.git" 8 files changed, 167 insertions(+), 3 deletions(-)  $ git revert 244d174 [develop 3cebd68] Revert "Support classes again" 45 files changed, 557 insertions(+), 1572 deletions(-) (list of affected files)  $ git revert a97a877 error: could not revert a97a877... Pruned all unused references (again). hint: after resolving the conflicts, mark the corrected paths hint: with 'git add <paths>' or 'git rm <paths>' hint: and commit the result with 'git commit'  $ git mergetool Merging: exampleFile1.cs exampleFile2.cs  Deleted merge conflict for 'exampleFile1.cs': {local}: deleted {remote}: modified file Use (m)odified or (d)eleted file, or (a)bort? m  Deleted merge conflict for 'exampleFile2.cs': {local}: deleted {remote}: modified file Use (m)odified or (d)eleted file, or (a)bort? m  $ git commit -m "Adding files to be reverted along with the next commit." [develop 15bc02b] Adding files to be able to revert the next commit in line. 2 files changed, 239 insertions(+) (list of affected files here)  $ git revert -m 1 8c29252 # On branch develop # Your branch is ahead of 'origin/develop' by 3 commits. #   (use "git push" to publish your local commits) # # Untracked files: #   (use "git add <file>..." to include in what will be committed) # #       exampleFile1.cs.orig #       exampleFile2.cs.orig nothing added to commit but untracked files present (use "git add" to track)  $ git revert a78b993 [develop 841e77c] Revert "Support models & methods - product types & categories" 2 files changed, 239 deletions(-) (list of affected files here) 

Commit log after all the reverts were done:

$ git log develop -10 --pretty=oneline --abbrev-commit 841e77c Revert "Support models & methods - product types & categories" 15bc02b Adding files to be able to revert the next commit in line. 3cebd68 Revert "Support classes again" 40965a5 Revert "Merge branch 'develop' of <our_repo_path>.git" faada93 Merge branch 'develop' of <our_repo_path>.git 244d174 Support classes again a97a877 Pruned all unused references (again). 8c29252 Merge branch 'develop' of <our_repo_path>.git a78b993 Support models & methods - product types & categories da8b496 Resolved JIRA issue PPF-182 

Graph after reverts:

$ git log --graph --oneline -8 develop * 841e77c Revert "Support models & methods - product types & categories" * 15bc02b Adding files to be able to revert the next commit in line. * 3cebd68 Revert "Support classes again" * 40965a5 Revert "Merge branch 'develop' of <our_repo_path>.git" *   faada93 Merge branch 'develop' of <our_repo_path>.git |\ | * a97a877 Pruned all unused references (again). | *   8c29252 Merge branch 'develop' of <our_repo_path>.git | |\ | | * da8b496 Resolved JIRA issue PPF-182 

Seems correct to me. Lastly, I remove some backup files that I don't want to keep:

$ git clean -fd (list of affected files here) 

Current status is clean:

$ git status # On branch develop # Your branch is ahead of 'origin/develop' by 4 commits. #   (use "git push" to publish your local commits) # nothing to commit, working directory clean 

And then I push everything back to the remote:

git push origin develop 
like image 562
leifericf Avatar asked Aug 06 '13 14:08

leifericf


People also ask

How do you revert a merge commit and push?

simply run git reset --hard to revert all those changes.

Can we revert multiple commits in git?

We can use the git revert command for reverting multiple commits in Git. Another way of reverting multiple commits is to use the git reset command.


1 Answers

Even though your history has changed, you can create branches that let you go back and experiment. Git means never having to say, “you should have.” If you converge on a reality you like better, then go with it. Otherwise, throw it away.

The examples below will create new branches that leave everything else in your repository alone.

Alternative 1: git revert

First create a scratch branch at the point where you started your adventure.

$ git checkout -b tmp-revert faada93

By specifying a commit range, git revert will undo multiple commits.

$ git revert da8b496..faada93

Alternative 2: git commit-tree

Consider the diagram below from Git Internals — Git Objects, section 10.2 in the second edition of Pro Git by Scott Chacon and Ben Straub. The topmost commit (“third commit”) has a SHA1 hash that begins 1a410e. In the context of this history, 1a410e^{tree} would resolve to 3c4e9c, that is, the tree object immediately to the third commit’s right.

Git Object Graph, Figure 151 from *Pro Git* Figure 151 from Pro Git, 2nd ed.

Study this model to understand how git tracks content. Creating a new fourth commit whose tree is identical to the second commit’s (that is, 0155eb) would add a new commit object that would share or “point to” the existing tree and blobs rather than adding new duplicate objects.

Read on to learn how to perform this low-level stitching with git commit-tree.

Start by creating another temporary branch to work on.

$ git checkout -b tmp-ctree faada93

At this point, you want to create a new commit where its tree (that is, the committed code) is identical to that of da8b496, the last commit you wanted to keep. This tree is directly addressable in git: da8b496^{tree}.

git commit-tree is “plumbing,” a low-level command in git—as opposed to “porcelain.” It may feel awkward or unfamiliar to use, but in this case it gives precise control of the result you want.

Create a new unattached commit whose tree is the same as da8b496’s and whose parent (-p) is the tip of the current branch, faada93 in your case. Note that git commit-tree reads the commit message of the new commit on the standard input, which the command below supplies with the echo command.

$ echo Revert back to da8b496 | \     git commit-tree da8b496^{tree} -p $(git rev-parse tmp-ctree) new-commit-sha1

The italicized portion above is not part of the command. It indicates that git commit-tree outputs the SHA1 hash of the newly created commit. Knowing the new commit’s SHA1, you can move the branch to that point, e.g.,

$ git merge new-commit-sha1

In the command above, replace new-commit-sha1 with the output from git commit-tree. (You could do the same git reset --hard new-commit-sha1, but hard reset is a sharp tool where casual use is best avoided.)

You could roll all of the above into a single compound command.

$ git merge --ff-only $(echo Revert back to da8b496 | \     git commit-tree da8b496^{tree} -p $(git rev-parse tmp-ctree))

The --ff-only switch to git merge is meant to prevent surprises. Your intent is for the new commit to be a fast-forward or a descendant of the current branch head—its immediate child, in fact!

Cleanup

To delete the temporary branches above, switch to another and fire away, Mr. McManus. Your other branches will be just as you left them.

$ git checkout develop $ git branch -D tmp-revert tmp-ctree

The two should be identical, as you can verify with

$ git diff tmp-revert tmp-ctree

To keep one, merge it into your develop branch.

$ git checkout develop $ git merge --ff-only tmp-ctree $ git push origin develop
like image 132
Greg Bacon Avatar answered Sep 28 '22 04:09

Greg Bacon