Revert a commit on remote branch

TL;DR

Remember that git revert really means back out a change, not revert to, i.e., restore, some particular version. It is possible to achieve revert to / restore, but the commands that do this are git checkout and git read-tree (both of these are mildly tricky, for different reasons). See How to revert Git repository to a previous commit? and Rollback to an old Git commit in a public repo (specifically jthill's answer).

The only really tricky part here is "revert a merge". This amounts to using git revert -m 1 on the merge commit, which is easy enough to run; but it means that afterward, you cannot re-merge as Git is quite certain (and correct) that you already merged all of that work and that the correct result is in place (which it is, you just undid it later). To put it all back, you can revert the revert.

Reverting makes a new commit, which like every commit, just adds a new commit to the current branch. You do this in your own repository as always. The rest of the job is just to push the new commit to some other Git repository, adding it to that other Git's collection as the new tip of one of their branches.

Long (goes into lots of detail)

First, let's back up a bit, because the phrase remote branch means nothing, or rather, means too many different things to too many different people—in either case, it winds up resulting in failed communication.

Git does have branches, but even the word "branch" is ambiguous (see What exactly do we mean by "branch"?). I find that it is better to be specific: we have branch names like master and staging and, in your suggestion, cleaning. Git also has remote-tracking names or remote-tracking branch names like origin/staging, but confusingly, Git stores those names locally, i.e., in your own repository (only). Your Git uses your origin/staging to remember what your Git saw on their Git, when your Git was last talking with their Git and asked them something about their staging.

Hence, the root of the problem here is that your Git repository is yours, but there is a second Git repository that is not yours, and you ultimately would like to do something on that other Git repository. The constraint here is that your Git only lets you do these things in your repository, after which you will eventually run git push. The git push step will transfer some commit or commits from your Git repository over to their Git repository. At this time, you can ask their Git to set their name—their staging—and they will either say Yes, I have set that (your Git will now update your origin/staging to remember this), or No, I refuse to set that, for the following reason: _____ (insert reason here).

Hence, what you are going to do in your repository is set up the appropriate steps so that their Git, in their repository, will accept the commit you git push and will update their staging. Keep that in mind as we go through these steps. There are multiple ways to do this, but here are the ones I would use.

  1. Run git fetch. This command is always safe to run. You can give it the name of one specific remote, if you have more than one, but most people only have one, named origin, and if you only have one, there's no need to name it.

    (The name of the remote—origin—is mainly just a short-hand way of spelling the URL that your computer should use to reach the other Git repository.)

    This git fetch may do nothing, or may update some of your remote-tracking (origin/*) names. What git fetch did was call up the other Git, get from it a list of all its branches and which commit hashes go with them, and then bring over any commits they have that you don't. Your origin/staging now remembers their staging. It's now possible for you to add a new commit to this.

  2. Now that your origin/staging is in sync with the other Git's staging, create or update a local branch name as needed. The best name to use here is staging, but you can use cleaning if you like. Because this step is create or update, it has sub-steps:

    • If it's "create", you can use git checkout -b staging origin/staging to create a new staging whose upstream is origin/staging.

      You can shorten this to git checkout staging, which—since you don't have your own staging—will search through all of your origin/* names (and any other remote-tracking names, if you have more than one remote) to find which one(s) match. It then acts as though you used the longer command. (With only one remote, the only name that can match is origin/staging. If you had both origin and xyzzy as remotes, you could have both origin/staging and xyzzy/staging; then you'd need a longer command.)

    • Or, if it's "update", you will already have a staging that already has origin/staging set as its upstream, because you have done this before. In this case, just run:

      git checkout staging
      git merge --ff-only origin/staging
      

      to get your staging re-synchronized with origin/staging. If the fast-forward merge fails, you have some commit(s) they don't, and you'll need something more complex, but we'll assume here that this succeeds.

      You can abbreviate these commands a bit as well, but I'll leave them spelled out here. Note that the first command is the same as the short version for the first case above, and you can tell which one happened by the output from git checkout staging. (I'll leave the details for other questions or an exercise.)

    We can draw a picture of what you have now, in your own repository, and it looks something like this:

    ...--o--o--o---M   <-- staging (HEAD), origin/staging
             \    /
              o--o   <-- feature/whatever
    

    Each round o represents a commit. M represents the merge commit, whose result you don't like, but which is also present in the other Git at origin under their name staging, which is why your own Git has the name origin/staging pointing to commit M.

  3. You now want to create a commit that undoes the bad commit. This will likely use git revert, but remember, revert means undo or back out, not switch to old version. You tell Git which commit to undo, and Git undoes it by figuring out what you did, and doing the opposite.

    For instance, if the commit you say to revert says "remove the file README", the change will include "restore the file README in the form it had when it was removed." If the commit you say to revert says "add this line to Documentation/doc.txt", the change will include "remove that line from Documentation/doc.txt". If the commit you say to revert says "change hello to goodbye" in some third file, the change that revert will do is to change "goodbye" to "hello" in that third file, on the same line (with some magic to find the line if it moved).

    This means that git revert can undo any commit, even if it's not the latest commit. To do so, though, it must compare that commit to its immediate parent. If the commit you are attempting to revert is a merge commit, it has more than one parent and you will need to specify which parent Git should use.

    The correct parent to use is not always immediately obvious. However, for most merges, it's just "parent number 1". This is because Git places special emphasis on the first parent of a merge: it's the commit that was HEAD when you ran git merge. So this is everything that the merge brought in, that was not already present.

    When the git revert succeeds, it makes a new commit that undoes the effect of the merge:

                     W   <-- staging (HEAD)
                    /
    ...--o--o--o---M   <-- origin/staging
             \    /
              o--o   <-- feature/whatever
    

    Here, W represents this new commit: it's M turned upside down. All you have to do now is run git push origin staging to send your own new commit W to the other Git:

  4. git push origin staging: this calls up that other Git and offers it commit W—that's every commit we have that they don't; they have M and everything earlier (to the left), but not W.

    As long as there are no special restrictions, they will accept this new commit and change their staging to point to new commit W. Your Git will remember the change:

                     W   <-- staging (HEAD), origin/staging
                    /
    ...--o--o--o---M
             \    /
              o--o   <-- feature/whatever
    

    (There's no need to keep drawing W on a separate line, but I am using copy-paste here to keep the shape the same.)

As you can see, you're now done. You and they both agree that your and their staging should both point to commit W that has the effect of undoing commit M. It's now safe to delete your own staging name, if you like:

git checkout <something-else>
git branch -d staging

which produces:

...--o--o--o---M--W   <-- origin/staging
         \    /
          o--o   <-- feature/whatever

Don't make it complicated.

First you need to do a git log to find out which commit ID you want to revert. For example it is commit abc123. If you know that it's the last one, you can use a special identifier "HEAD".

Then you first revert it locally in your local "staging" branch:

git checkout staging
git revert abc123

Note: for the last commit in the log you would write git revert HEAD.

And then you update your remote "staging":

git push

Explanation: In git if you have a remote you are dealing with 2 distinct repositories (local and remote). After you do git checkout staging, you actually create a distinct local name that represents the remote branch. git revert actually doesn't delete your commit, but it creates a new commit on top, that undoes all the changes (if you added a file - the new commit will remove it, if you removed a line - the new commit will add it back etc.), i.e. it is an addition to the log, that's why the push will be clean.

If you want to really make it gone, so that nobody can blame you, you can do at your risk:

git checkout staging
git reset --hard HEAD^
git push -f

(the reset line retargets the local "staging" branch so that it points to a commit that is right before your top commit)

In general the forced push is a bad practice, but keeping useless commits and reverts around is not nice as well, so another solution would be to actually create a new branch "staging2" and do your testing in that branch instead:

git checkout staging
git checkout -b staging2
git reset --hard HEAD^
git push

Tags:

Git