Revert part of a large commit based on a regex grep pattern in the code
Let’s say you have a big commit and you have some changes merged into that commit that you would like to revert. Unfortunately, you didn’t commit those changes as separate commits, so reverting is non-trivial.
Here is a concrete example: at The RealReal we are in the process of upgrading from Rails 3.2 to Rails 4.2. A lot of the changes were made and then squashed over time to make rebasing easier.
In the middle of this large squashed commit, a change was made to remove attr_accessible from all the models. We later decided to use the protected_attributes gem, based in part on How to Upgrade to Strong Parameters in Rails and also because of the huge amount of work involved. Now we need all those attr_accessible lines added back in.
Instead of manually finding each line in the diff and copying it back in, I discovered and used the following process. Get ready to resolve some merge conflicts.
The basic idea is to split the 1 big commit into 2 commits:
the commit that removed attr_accessible (which we want to revert)
the rest of the changes on your branch
It’s important here that you turn off rerere in your .gitconfig. The first time you run into the merge conflicts they will be when you are making a commit that remove attr_accessor. The second time through, we will resolve the commits in the opposite way (keeping attr_accessor). rerere confuses this by always picking our first version and throwing out the changes we want.
$ git config --global rerere.enabled false
Now we need patchutils, which gives us tools for grepping within a diff. I’m assuming here that you have homebrew installed.
$ brew install patchutils
Now view the diff, output it as a patch, and look at the output to see if the match string is getting the right changes.
$ git diff master…rails4 -U0 | grepdiff 'attr_accessible' --output-matching=hunk
Make a patch if it looks mostly good. grepdiff is unable to perfectly exclude unmatching changes that are part of the same hunk.
$ git diff master…rails4 -U0 | grepdiff 'attr_accessible' --output-matching=hunk > remove_attr_accessor.patch
Unfortunately, this patch isn't quite right because it includes chunks we don't want that grepdiff is unable to separate.
Next, checkout a fresh branch at the base of the rails4 branch. This keeps us from dealing with a rebase onto the current master.
$ git checkout master...rails4
Create a branch to work on.
$ git checkout -b rails4-attr-accessor
Apply the patch to the new branch.
$ git apply --unidiff-zero < remove_attr_accessor.patch
The next step is more complicated. We need to manually pick only the attr_accessible lines. The only way I know how to do this is by manually adding only the parts of the patch that we truly want. You could also look at the diff and change it so that only the lines we want are actually changed.
$ git add -p
Remember that you’re making a patch that removes attr_accessor. This is the opposite of what we eventually want to do and it can be confusing when you’re working on a file.
Eventually you will have a staged commit that only removed attr_accessor. Check in the commit.
$ git commit -m “Remove attr_accessor"
Now we can just make a revert commit right on top of this branch to make an “inverse” commit that adds attr_accessor back.
$ git revert HEAD
Now checkout the main branch, and cherry-pick the revert commit on top.
$ git checkout rails4
$ git cherry-pick rails4-attr-accessor
This will create a revert commit on the very top of the rails4 branch that adds attr_accessor back in to your branch. I suggest leaving it like this, rather than trying to squash or re-order the commits with a git rebase -i. It can lead to tons of merge conflicts and is just a waste of time.
At this point, you should have the changes restored as commit on the top of your main branch. I hope this helps save you a bunch of manual work and mistakes.










