Knows Not Much
Knows Not Much

Reputation: 31526

Undoing latest push to remote branch

I accidentally committed and pushed my code changes to the wrong branch.

Here is what I have done to undo my bad changes

  1. git log : find out where I need to go back to
  2. git reset --hard 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546
  3. git push -f origin 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546:develop

but I get an error

! [remote rejected] 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546 -> develop (protected branch hook declined)

How do I undo on the remote as well? I guess git reset --hard is only local not remote.

Upvotes: 2

Views: 2558

Answers (2)

torek
torek

Reputation: 487755

I put in a comment, but this is (much) too long as a comment, so I'll put this in as an answer as well. It's not clear to me what you have done both locally and in any other (push-able) repositories, at this point.

Distributed repositories

First, keep in mind that every time you use Git in a distributed manner—on your own machine, and with a "remote" like origin—that there are two (or more) separate Git repositories involved. Things you do locally are, indeed, purely local: you do them in your repository, and now you have them, but no other repository has these commits.

To send commits from your repository to another, you can either push them:

git push origin refspec

or talk to whoever runs the other repository—let's say it's your friend Bob—and convince him to run git fetch to your repository.

When the remote is a big server like GitHub, so that you don't actually have a friend Bob running it, the server folks usually set things up so that you have some sort of interface (such as a web browser method) by which you can make changes. For instance GitHub allows you to "pretend to be Bob" and do things like protect and unprotect branches by their name on the GitHub server.

The commit graph

Next, remember that branch names in Git are just treated as moveable pointers to commits in a commit graph. The commit graph is much more permanent and solid: once a commit exists, it exists in that exact form "forever". Branch names, however, can be added, removed, and moved around. So it's the commits, which have as their "true names" those big ugly hash IDs like 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546—that are the main concern—but you almost always access the commits through things like branch names.

(Branch names also protect commits, as well as make them accessible, though for the moment that's not something you need to worry about.)

Draw your commit graph

These things always make more sense if you draw (at least part of) your commit graph. You may have a branch name like develop that "points to" (has the ID of) a commit like 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546. That particular commit itself, however, points to (contains the ID of) another, earlier, commit. Every commit points back to its parent commits, and this means we can draw the linkage of commits using arrows. I don't actually have arrows so I use approximations here:

... <- o <- o <- o   <-- develop

Here, each commit is just a round o pointing back to its parent. The branch name develop points to the most recent or tip commit of the branch. That commit points to its parent, which points to another parent, and so on.

All the intermediate arrows tend to be just a distraction (plus they're kind of backwards) so I prefer to draw these as:

...--o--o--o   <-- develop

If we have develop "branching off" from another branch like master, the drawing makes it pretty clear how this works:

...--o--o--o--o   <-- master
      \
       o--o--o    <-- develop

git reset moves branch labels

When you use git reset, it lets you move your branch labels. This has no effect on the graph itself, only on where the label points. Let's say that you are on branch develop and you use git reset to "remove" the last commit from develop. The picture now changes to:

...--o--o--o--o   <-- master
      \
       o--o--o
          ^
          |
       develop

See how the graph is totally unchanged, and only the label has moved? The old tip of develop—the commit itself, that is—is still there. It's just that it no longer has a branch name pointing to it. To name that particular commit, you now have to use its hash ID, which you may or may not have saved anywhere.

(Well, it has no such name in this drawing. But did we draw in all the branch names? If not, is there some other human-readable name that points to it? Again, let's not worry about that for the moment.)

Before git reset, if you ask your Git to show you the commit you call develop, it shows you the old tip: the rightmost o commit on the line there. After git reset, if you ask your Git to show you the commit you call develop, it shows you the middle o commit on that same line. So your Git now thinks that develop means that commit and all the earlier ones.

git push asks the other Git to change where its names point

As you suggested in your question, doing the git reset only changes your Git's idea of which commit develop names. Any other repository that has the name develop pointing to some other commit—such as the previous tip—still has their name pointing to the old tip.

Using git push, you can ask any other Git that you can connect-to, to change their idea of where any branch name should point. That other Git has the right to say "no", and there are two "levels" you can use when doing git push: a sort of polite request, "please do this", and the more forceful "do this!" that you get with --force. The other Git can still say "no" to one or both such operations.

What happens when you make a bad commit

You say in your question that you:

commited and pushed my code changes to the wrong branch.

It's not clear to me whether you committed to the wrong branch, then pushed that branch by name, or whether you committed to the right branch, then pushed the wrong name, or what. It's also not clear whether your push succeeded in the first place, since the only example output you gave has an error:

git push -f origin 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546:develop
! [remote rejected] 3cd4e57dcbb2a5bae350086c11d64c2f01ad4546 -> develop
(protected branch hook declined)

This was a force-push, and they still said "no". The error message here uses the phrase "protected branch", which is a GitHub feature, but which is also a GitLab feature, and could come from pretty much anywhere. So I'm not sure which protection feature this refers to. All I can be sure of is that they said "no", and not really why they said "no". And, in particular, I cannot tell from this whether any earlier push succeeded.

Let's take a closer look at what happens with bad commits in either case, though. Let's begin with this graph:

...--o--o--o--o   <-- master
      \
       o--o       <-- develop

If you're on branch develop and you make some changes to your work-tree and git add and git commit, Git makes a new commit with the new snapshot. Let's say it's not a good change though, and let's call it B for Bad:

...--o--o--o--o   <-- master
      \
       o--o--B    <-- develop

If you have not pushed this anywhere, then by definition, nobody else has the bad commit. That makes it easy for you to git reset it away:

...--o--o--o--o   <-- master
      \
       o--o--B
          ^
          |
           \--------- develop

and now we can simply forget about B, pretend it never existed, shove it off down to the side and start ignoring it:

...--o--o--o--o   <-- master
      \
       o--o       <-- develop
           \
            B     [abandoned]

A commit that is abandoned like this eventually expires and really does get removed (normally, some time after 30 days have gone by—the abandoned commit is no longer protected by the branch name, but normally remains protected by a reflog entry, and these reflog entries eventually expire, after a configurable amount of time, the default being 30 days).

You have another option to get rid of B, though. Just make the same change in reverse. If you added a line to file blah.html, remove that line. If you changed a word in main.py, change it back. Whatever you did, do the exact opposite. Then, make a new commit. Let's call it U for Undo:

...--o--o--o--o     <-- master
      \
       o--o--B--U   <-- develop

The code associated with commit U—the working tree—will match the code for the commit right before B, because B does something bad, and then U undoes it.

Making commit U is called reverting,1 and there is a command in Git that does this without any extra work on your part: git revert. You give the revert command something to name the bad commit—such as its hash ID, or even just the name develop when B is the tip commit on branch develop—and Git figures out what changed in the bad commit, and makes a reverse commit.


1Well, Git calls it "reverting". Others call it "backing out": in Mercurial, the command that "backs out" a commit is hg backout.


The difference between reset and revert

The difference between doing a reset to remove a bad commit, and doing a revert to add a new commit that undoes the bad one, is precisely that: reverting adds stuff. It leaves the bad commit in place and simply adds a new commit that backs out the effect of the bad commit.

Git is built around adding new commits

All of Git is built around this whole idea of "adding new stuff". New commits get added on top of older commits. Everyone and everything is prepared to deal with this. Some of Git is built to deal with removing commits, but all of Git is built to deal with adding them.

If you have not pushed a bad commit anywhere else, nor allowed anyone else to fetch it from you, then obviously no one else has it. This means it is easy to remove the commit from—or at least, abandon it within—your own repository. Your Git will deal with that. But if you have pushed the commit, or let it get fetched, then someone else has it. If you take it away, you must get them to take it away too. If one other person has it, he may have given it to someone else, and by now there could be thousands of copies of it out there.

In this case, reverting is easier for everyone else. They are all prepared to get new commits, and if you add a new revert commit, they pick it up in the usual way: no fuss, no muss, everyone does what they all do all the time. So for this purpose revert is usually better. Of course, it leaves the bad commit behind—but maybe that's even a good thing. It may act as a lesson: "don't try this, it's wrong". :-)

If your protected branch only allows new commits

If the protection on the protected branch allows new commits to be added, but does not allow "removing" (which is perhaps better called "abandoning") old commits, a revert-and-push will work.

If the protection is something else, perhaps nothing will work.

If the protection can be turned off (temporarily or otherwise), that may allow both force-push to abandon a commit, and revert.

Remember that if you force-push, you are racing against anyone else who may also be pushing. When you force-push, you're telling some other Git to change the commit ID where some branch points. When you do this, you are probably assuming that it points to your bad commit right now. But what if someone else has done a new push, after your bad commit B, that uses your commit B? Your force-push will abandon their new commit. Be sure that's OK—and if not, find some way to make sure it's not going to happen.2


2This is where --force-with-lease comes in, but I'm not going to address that here.

Upvotes: 2

janos
janos

Reputation: 124648

It seems the remote branch is protected. To make the force-push work, you can temporarily unprotect the branch, push, and protect again. You can do this on the Settings / Branches tab of your repository's page on GitHub. Note that this (and force-pushing in general) is not a recommended operation on public repositories.

Alternatively, you can undo the commit by reverting it, which will generate a new commit after the bad one, and then simply push.

Upvotes: 4

Related Questions