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.
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, namedorigin
, 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. Whatgit 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. Yourorigin/staging
now remembers theirstaging
. It's now possible for you to add a new commit to this.Now that your
origin/staging
is in sync with the other Git'sstaging
, create or update a local branch name as needed. The best name to use here isstaging
, but you can usecleaning
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 newstaging
whose upstream isorigin/staging
.You can shorten this to
git checkout staging
, which—since you don't have your ownstaging
—will search through all of yourorigin/*
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 isorigin/staging
. If you had bothorigin
andxyzzy
as remotes, you could have bothorigin/staging
andxyzzy/staging
; then you'd need a longer command.)Or, if it's "update", you will already have a
staging
that already hasorigin/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 atorigin
under their namestaging
, which is why your own Git has the nameorigin/staging
pointing to commitM
.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 rangit 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'sM
turned upside down. All you have to do now is rungit push origin staging
to send your own new commitW
to the other Git:git push origin staging
: this calls up that other Git and offers it commitW
—that's every commit we have that they don't; they haveM
and everything earlier (to the left), but notW
.As long as there are no special restrictions, they will accept this new commit and change their
staging
to point to new commitW
. 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