Reverting Distinct Git Commits

Posted on Mon 10 October 2016 in Git

The Cloud

A developer contacted me with a problem: he had created a clone of a pipeline in GoCD that was created from a template, and was making changes to that pipeline, thinking that he was editing his clone. Seems legit.

What he didn't realize was that, in GoCD, changes to a template apply immediately to all other pipelines created from that template. Yikes.

His first changes started on Friday morning and it was now Monday afternoon. That means there have been many changes to the config from many other people since then that would have to be preserved, while extracting only his changes. Double yikes.

The Silver Lining

GoCD saves any changes made to its main config file (cruise-config.xml) in an internal git repo. This was a relief, as I could see what happened when, and have a fighting chance to revert it.

The Challenges

Because other people's changes (commits) were interspersed with the changes I needed to pull out, I'd have to figure out how to get only that user's changes.

In addition, I couldn't just "force push" a rebased branch into the internal git repo, because there may have been associated info with those commits that existed in the app's underlying database.

The Solution

First, finding that specific user's commits was thankfully simple - I just grepped the log for that user's name, and got each commit hash:

Here's a sample of the output GoCD has of its internal repo:

38c8da1 user:anonymous|timestamp:1476131511596|schema_version:82|go_edition:OpenSource|go_version:16.6.0 (3590-af0f8b8ae8a4342ab748ce8022
803fa79 user:jack.mcgaw|timestamp:1476131062982|schema_version:82|go_edition:OpenSource|go_version:16.6.0 (3590-af0f8b8ae8a4342ab748ce8022
58df619 user:anonymous|timestamp:1476131052990|schema_version:82|go_edition:OpenSource|go_version:16.6.0 (3590-af0f8b8ae8a4342ab748ce8022
7f31528 user:anonymous|timestamp:1476130458484|schema_version:82|go_edition:OpenSource|go_version:16.6.0 (3590-af0f8b8ae8a4342ab748ce8022
a4e449d user:jack.mcgaw|timestamp:1476126266851|schema_version:82|go_edition:OpenSource|go_version:16.6.0 (3590-af0f8b8ae8a4342ab748ce8022
89fc7b5 user:nisa.chazon|timestamp:1476126266520|schema_version:82|go_edition:OpenSource|go_version:16.6.0 (3590-af0f8b8ae8a4342ab748ce8022

So, I grepped for the user I needed ("jack.mcgaw" in the above example) and pulled out the commits:

$ git log --oneline -100 | awk '/jack.mcgaw/ {print $1}'

This gave me a list of 18 commits in total that needed to be reverted. To confirm that date/time was correct, I used an online epoch converter and verified the commit + time with the developer after finding what I thought was probably the first of his changes.

At this point, had this been a typical Github repository, I could have interactively rebased the branch, marked each commit from this list as deleted, force pushed, and called it a day. However, I wasn't sure if the app's database had some tie-in with this config repo, so I didn't want to risk corrupting the DB through a force push.

This small repo simply tracked the changes to one file. How could I undo the changes to this file from discrete, non-contiguous commits?

The Patches

Old school ftw! My first attempt, just like any developer, was to over-engineer what eventually was a simple solution - use patches.

I created a patch for each commit listed and figured I could just reverse-apply them in sequence to the config file. Genius!

But of course, I wanted to number each file so I could easily see said sequence, and more easily pass them as an ordered list. Then I could easily loop over them and reverse-apply each one to the config file. So simple, right??:

$ a=0 ; for c in $(git log --oneline -100 | awk '/jack.mcgaw/ {print $1}'); do [[ $a -lt 10 ]] && prefix="0${a}" || prefix="${a}" ; git format-patch -1 --stdout $c > $prefix-$c.patch; a=$((a=a+1)) ; done

This worked. But, look at it. Yuck.

The Refactor

Creating a patch still seemed like a solid idea, but I could already tell this was shaping up to be too complicated to what was basically just applying a patch to file.

How about if I just created one big patch which I could apply to the file?

$ for c in $(git log --oneline -100 | awk '/jack.mcgaw/ {print $1}'); do git format-patch -1 --stdout $c >> big.patch ; done

$ git apply -R big.patch

Ah, much better. And it worked like a charm.

I copied the entire resulting file, and pasted it into the "Config XML" editor in GoCD's UI, thinking that doing so would be a "legitimate" change to GoCD, so any/all DB updates & XSD adjustments would happen accordingly. I was right (yay!).

Bonus: I was able to share the "big.patch" file contents with the developer before applying it to further verify if these changes were the ones he expected to get reverted (they were).

The Conclusion

Google easily found documentation for me on how to get the changes within a range of git commits. But it wasn't easy for me to find docs on getting the changes from discrete, non-contiguous commits spread out over time from one user. In addition, having the constraint of not being able to rebase the branch was a departure from my usual git toolbox of tricks.

As always, I hope you found this interesting, and saves you extra time digging around the interwebs.