Customizing Git with Ruby Git Hooks
Remember the last time you accidentally checked in a gigantic file you didn’t mean to, and had to dig through the Git history for it? And everybody had to update their history and that one guy accidentally blew away all his changes?
Ah, Git . It’s a useful and powerful tool, but it’s also easy to make mistakes that can be a pain to recover from. What if you could avoid some of that pain before you commit your changes?
Over the past year, we have started using Git for some of our projects here at OnLive. As part of that process, we found the need for tools to help prevent mistakes we didn’t want to accidentally make – like checking in giant files, or forgetting to include a bug ticket number in the commit message.
Git does provide a mechanism for adding your own customizations, called Git Hooks, but it turns out to be kind of a pain to work with. Each type of hook interacts with Git in a different way, and most hooks are written in shell scripts. We wanted to take away the complexity and have a way to write our own checks in plain old Ruby, with the information we need at our fingertips
That turned into Ruby Git Hooks, which became OnLive’s first open-source software project!
How do I use it?
Once you install RubyGitHooks in your development environment, you can use the built-in hooks provided, or write your own.
If you want to catch that giant file before it gets committed to your Git repository, you can use the built-in MaxFileSizeHook. Set it up to run every time you do a git commit by putting the following into the file .git/hooks/pre-commit (that’s where Git looks for a pre-commit hook).
#!/usr/bin/env ruby # Put this file in .git/hooks/pre-commit # and make it executable! require "ruby_git_hooks/max_file_size" RubyGitHooks.run MaxFileSizeHook.new
That’s it! Next time you do a git commit, it will check for extra large files and stop the commit with a warning message if necessary. I don’t know why you had that gigantic file in the first place, but at least this can prevent it from wreaking havoc.
But wait, what if you want to check more than one attribute? For example, you also want to be sure you never commit files whose names differ only in case (like Fun.rb and fun.rb). With RubyGitHooks you can register multiple hooks and run them.
#!/usr/bin/env ruby # Put this file in .git/hooks/pre-commit and make it executable! require "ruby_git_hooks/case_clash" require "ruby_git_hooks/max_file_size" RubyGitHooks.register CaseClashHook.new RubyGitHooks.register MaxFileSizeHook.new RubyGitHooks.run
Can I Make My Own?
What if you also want to do something completely different, like, for example, check that your code passes unit tests before allowing the commit. There’s no built-in hook for that. But you can add your own with a few lines of ruby.
#!/usr/bin/env ruby # Put this file in .git/hooks/pre-commit and make it executable! require "ruby_git_hooks/case_clash" require "ruby_git_hooks/max_file_size" class UnitTestHook < RubyGitHooks::Hook def check # return a boolean value system(“rake test”) end end RubyGitHooks.register CaseClashHook.new RubyGitHooks.register MaxFileSizeHook.new RubyGitHooks.register UnitTestHook.new RubyGitHooks.run
This is a very simple example, but the great thing is that within your hook you have access to information about the commit from git – including which files are in the commit, the diffs, the commit message and even which kind of hook we are being run as. You can look at the code for the built in hooks to see how easy it is to get at this information.
Is that all?
RubyGitHooks also supports other types of hooks besides just pre-commit and has a variety of other built-in hooks. It’s been a valuable tool in our use of Git and we would love to hear how it’s useful (or not) for you too!
: RubyGitHooks supports local (pre-commit, commit-msg, and post-commit) and server side hooks (pre-receive and post-receive). But the server side hooks can’t be used with repositories hosted on GitHub, because you can’t install arbitrary code on even a GitHub Enterprise server. But that’s another story (stay tuned…)