Tuesday, July 9, 2013

Git Tip - Reverting latest bad commit(s), and which have also been pushed already

By and large, working with Git is "not that scary" (TM) once you figure out a safe subset of commands which will let you achieve what you want to do during a typical work session.

However, from time to time, especially when effectively "working blind" via commandline, things go wrong. Really really wrong.

In my case, as I don't have admin rights to install stuff on my office computer, and haven't been able to work around the problem of not being able to use Git GUI to do most of my Git-based work by compiling a local copy to go in my HomeDir (it was something about not having some tcl index file, and then other crap about other libraries not being found, and also other complains about libraries not being found), I've been stuck using the commandline version of Git (at least until I manage to have this installed by the admins sometime).

Anyways, this afternoon, I ran into quite a nasty little situation, whcih took the better part of the past half-hour to try and resolve.

Like, when writing a commit message in Vim, you accidentally save off the buffer as a new file named ":q" in the root directory, proceed to accidentally stage this for the next commit, which you don't really notice until having just typed up a commit log. Ah, bugger. Let's try to just unstage it... oh wait, this is the commandline version, so you need to use some arcane invocation that has nothing remotely to do with "unstage" or "revert"... if only I could remember/find what the hell it was!

After several failed attempts, I ended up just letting it commit the darned thing along with that commit. Then, the next thing would be to issue a
$ git rm --cached ":q"
which seemed to do the trick last time there was an unwanted file accidentally committed. Bugger. Failed again. If I do this, not only is the file not removed from the existing commits (at this point, this was *just* the last commit locally), but, the next pending commit would have a big fat "':q' deleted" component to it. Blegh! Oh well, let's play along with this...

Figuring that it might be possible to easily revert this when back home again, I decided to push to my central repository. Great... now we have two unwanted commits there... But, then I still needed to carry on working, and carrying on with these commits there would be hazardous. So, I tried again to remove the blasted thing.

After one more failed attempt at simply erasing all traces of the offending file from the repository, I decided to try a more radical strategy: completely back off the last two commits, commit a new one (with only the valid changes in place), push that onto the server to completely erase any trace of the bad commits, and then finally get back to work.

Of course, everything out there says that this is a nasty and evil thing to do... "think about all the poor little devs whose repositories will now be in an invalid state and unable to sync". Well, in my case, there is only one other dev/repo that needs to actively work with this, and that's me on my other computer, which is a whole afternoon's work behind anyway, so there's no conflict potential at all. Hah! Moving right along...

The Method
1) git reset --hard HEAD~2
This gem basically grabs the branch's pointer, and makes it point to the commit from just before the bad ones - i.e.  [... old commits ..., HEAD~2, HEAD~1, HEAD]. Great, if we just make a new commit now, the old ones are effectively gone from the history (and gone for good, if we run a garbage collect on the commit graph after we've made a new commit)

2) Go back to text editor, where the changes we'd been meaning to commit earlier are still on screen, and just force these to be saved to disk WITHOUT RELOADING the file first (and thus losing our changes)

3) Commit this change normally (git add . ; git commit), this time taking great care to check that nothing else slipped in by doing git status just before doing the git add.

4)  git push -f
To now get the central repository back to a good state, we need to push (aka manually sync the good state over to the server). But, just doing a git push as per normal will apparently not work, as, as far as the git is concerned, the server has two more commits than we do. Ah, but we know better in this case (i.e. that's exactly what we're trying to fix!), so adding the "-f" (or "--force" flag) lets you say, "whatever! just do what I say!".

Finally, the repository was back to a valid state, having just wasted an hour trying to solve this otherwise simple problem which wouldn't have happened if using the standard gui. Gah!

No comments:

Post a Comment