Verifying every single commit in a Git branch by Mark Seemann
You can run a validation task for every commit in a Git branch in order to verify the integrity of a Git branch's history.
Just like the Soviet Union, I occasionally prefer to rewrite history... although I limit myself to rewriting the history of one or more Git branches. When I do that, I'd like to verify that I didn't break anything after an interactive rebase. It actually turns out that this can easily happen. Some commits are left in a state where they either don't compile, or tests fail. Git is happy, but the code isn't. To deal with such situations, I wanted a tool that could verify each and every commit in a branch like this:
- Check out a commit.
- Compile the code.
- Run all tests.
- Repeat for the next commit, or stop if there's a failure.
If you don't care about my motivation for doing this, you can skip this section and move on to the solution. However, some people might like to point out to me that I shouldn't rewrite the history of my Git repositories - and particularly not of the master branch. I agree, for all repositories where I actually collaborate with other programmers.
However, I also use Git locally to produce teaching materials. As an example, if you download the example code for my Pluralsight courses, you'll see that it's all available as Git repositories. I think that's an added benefit for the student, because not only can you see the final result, but you can also see all the steps that lead me there. I even occasionally write commit messages longer than a single line, explaining my thinking as I check in code.
Like everyone else, I'm fallible, so often, when I prepare such materials, I end up taking some detours that I feel will confuse students more than it will help. Thus, I have a need to be able to edit my Git repositories. Right now, as an example, I'm working with a repository with 120 commits on the master branch, and I need to make some changes in the beginning of the history.
With much help from Darrell Hamilton, who ended up providing this gist, I was able to piece together this bash script:
#!/bin/sh COMMITS=$(git --git-dir=BookingApi/.git --work-tree=BookingApi log --oneline --reverse | cut -d " " -f 1) CODE=0 git --git-dir=BookingApi/.git --work-tree=BookingApi reset --hard git --git-dir=BookingApi/.git --work-tree=BookingApi clean -xdf for COMMIT in $COMMITS do git --git-dir=BookingApi/.git --work-tree=BookingApi checkout $COMMIT # run-tests ./build.sh if [ $? -eq 0 ] then echo $COMMIT - passed else echo $COMMIT - failed exit fi git --git-dir=BookingApi/.git --work-tree=BookingApi reset --hard git --git-dir=BookingApi/.git --work-tree=BookingApi clean -xdf done git --git-dir=BookingApi/.git --work-tree=BookingApi checkout master
As you can tell, it follows the algorithm outlined above. First, it does a
git log to get all the commits in the branch, and then it loops through all of them, one by one. For each commit, it compiles and runs all tests by calling out to a separate build.sh script. If the build and test step succeeds, it cleans up the working directory and moves on to the next step. If the verification step fails, it stops, so that I can examine the problem.
(The reason for the use of
--work-tree is that I need to run the script from outside the Git repository itself; otherwise, the
git clean -xdf step would delete the script files!)
This has turned out to work beautifully, and has already caught quite a number of commits that Git could happily rebase, but afterwards either couldn't compile, or had failing tests.
Running those tests in a tight loop has also finally provided some work for my multi-core processor:
Each run through those 120 commits takes about 20 minutes, so, even though today we have fast compilers, once again, we have an excuse for slacking off.
A talk on some of Git's lesser known features that I recently saw suggested that there is a built-in mechanism for what you are trying to achieve. The
git bisectcommand allows you to run a script for each of a number of commits. The exact details on how to perform this are provided in Pro Git, so I won't repeat them here.
Thank you for writing. As far as I can tell from the documentation,
git bisectdoesn't quite do what I'd like to do. The bisect feature basically performs a search through the repository to figure out which commit introduces a particular error. The search algorithm is attempting to be as efficient as possible, so it keeps bisecting the list of commits to figure out on which side the error occurs - hence the name. Thus, it very explicitly doesn't check each and every commit.
My scenario is different. While I'm not looking for any particular error, I just want to verify that every commit is OK.
As of Git 1.7.12, the interactive rebase command can take an
--exec <cmd>option, which will make it execute
<cmd>after each commit. If the command fails, the interactive rebase will stop so you can fix the problem.
I didn't know about the
--execoption, but I'll try it out the next time I perform a rebase. Thank you for the tip.