Using Git to fix a typo in every commit of a branch

Justin -- 09 February 2013

Full disclosure: how I got into this situation is bad practice on my part but how I got out of it was interesting and I thought it would be worth sharing and potentially helpful to others.

Background

First, some background. I was creating a Moodle plugin to contain both a minified and uncompressed version of jQuery. There already exists a plugin in the community that fills this role–moodle-local_jquerybut it contains more than just jQuery and is meant to work as part of a larger component. It wasn’t exactly what I needed.

For my needs, I wanted a Moodle plugin which only contained jQuery and had the version property set to the specific release date of that version of jQuery. This allows another plugin to require a minimum version of my plugin in order to specify the minimum version of jQuery that it is compatible with. I needed this plugin to work with Moodle versions 2.2 through 2.4. So I went ahead and created a commit in a MOODLE_22_STABLE, MOODLE_23_STABLE and MOODLE_24_STABLE branch for each release of jQuery since 1.6.3 (everything listed on the jQuery download page).

After I had finished going through and creating all the branches I wanted to test my plugin on a local Moodle 2.2 install. I cloned the MOODLE_22_STABLE branch of the repository into /local/rljquery/ and ran through the installation process The process stopped before it had finished installing all the plugins with no error output. I looked through my web server error log file and saw this message:

2013-02-08 11:51:48: (mod_fastcgi.c.2676) FastCGI-stderr: PHP Parse error: syntax error, unexpected end of file in /Users/jfilip/code/moodle22/local/rljquery/version.php on line 33
PHP Stack trace:
PHP 1. {main}() /Users/jfilip/code/moodle22/admin/index.php:0
PHP 2. moodle_needs_upgrading() /Users/jfilip/code/moodle22/admin/index.php:248

Uh-oh.

I opened up the version.php file in my plugin and noticed the following:

$plugin->version   = 201302040; // Release date of this version of jQuery.
$plugin_requires   = 2011120500.00;
$plugin->component = 'local_rljquery';
$plugin->maturity  = MATURITY_BETA;
$plugin->release   = '1.9.1 (2.2)' // The jQuery release version with the Moodle release in parentheses.

So, I made two very simple syntax errors that I should have caught when first writing the code. The second line has $plugin_requires instead of $plugin->requires and the last line is missing a semi-colon at the end of the string and before the comment starts. In most cases this is a simple fix but in my case I had already spread this problem across 30 individual commits.

At that moment you would normally have to just throw everything away and re-do every single commit. But I was able to recall one time when I had done some digging into a way to perform changes to a Git repository across the entire history. So I did some Google digging.

The Nuclear Option: filter-branch

There is another history-rewriting option that you can use if you need to rewrite a larger number of commits in some scriptable way — for instance, changing your e-mail address globally or removing a file from every commit. The command is filter-branch, and it can rewrite huge swaths of your history, so you probably shouldn’t use it unless your project isn’t yet public and other people haven’t based work off the commits you’re about to rewrite. However, it can be very useful.

- http://git-scm.com/book/ch6-4.html#The-Nuclear-Option:-filter-branch

The filter-branch command in Git was what I had remembered. The documentation for the command has an example of using it to execute a shell script to re-write information about the user who performed a commit. I thought I could try this same approach to edit the contents of a file in every commit within a branch.

Eventually I came up with the following two commands which would fix each problem. It uses a perl command to do a search and replace within the version.php file for each commit within the current branch. Because git filter-branch creates a backup so you can revert the changes it has made if the results are not what you expected, I had to use the -f option in order to force the second command to overwrite the backup.

git filter-branch --tree-filter "perl -pi -e 's/plugin\_requires =/plugin\->requires =/g' version.php" HEAD
git filter-branch -f --tree-filter "perl -pi -e 's/ \/\/ The jQuery/; \/\/ The jQuery/g' version.php" HEAD

Next I had to check to make sure that the changes actually did get applied through the commits in the current branch. So I wanted to check the first commit in this branch to make sure it was modified correctly. I found the first commit using the following command:

git log --oneline | tail -n 1

bcbd130 Initial commit of jQuery 1.6.3 for Moodle 2.2

And then I wanted to look at just the relevant lines of that file to make sure the changes were made:

git blame bcbd130 -- version.php | tail -n 5

bcbd130 (Justin Filip 2013-01-23 11:25:26 -0500 28) $plugin->version   = 2011090100; // Release date of this version of jQuery.
bcbd130 (Justin Filip 2013-01-23 11:25:26 -0500 29) $plugin->requires  = 2011120500.00;
bcbd130 (Justin Filip 2013-01-23 11:25:26 -0500 30) $plugin->component = 'local_rljquery';
bcbd130 (Justin Filip 2013-01-23 11:25:26 -0500 31) $plugin->maturity  = MATURITY_BETA;
bcbd130 (Justin Filip 2013-01-23 11:25:26 -0500 32) $plugin->release   = '1.6.3 (2.2)'; // The jQuery release version with the Moodle release in parentheses

After verifying that this worked correctly, I then applied the same commands to the MOODLE_23_STABLE and MOODLE_24_STABLE branches of my plugin repository, repeating the same check to make sure the earliest commit had been updated as well.

One thing I thought was interesting is that even though I had just modified this commit, it didn’t update the commit timestamp with the current date. For more information about how the filter-branch command works, refer to the documentation page for it.

Conclusion

In the end I was able to fix the problems I had introduced, learn something new about Git and get my code into a state where it installed correctly and could be peer reviewed and tested within our internal process at Remote-Learner. I also re-learned that I should validate even very simple code before assuming it works and spreading it around to lots of places.