MiniGit: a simple Ruby interface to the Git command

Git logo

There is a handful of Ruby gems that work with Git repositories. All of them have one thing in common: they focus on the repository. They provide a more or less sophisticated object–oriented interface to the repository, commits, refs, etc – either wrapping libgit2 (like Rugged), or building their own implementation (like a sadly incomplete object-oriented API of Grit). They all come short when I need to talk with the Git command line — which is a proper API on its own. Grit was a closest match with its Grit::Git class, but to use it effectively I needed to not use rest of the library (and thus adding dead weight to my scripts), and deal with the fact that it always captures command’s output — there was no way to just call git diff, and have end user see the output in colors they defined and with pager they configured. To add insult to injury, when the Git command returns an error, Grit silently ignores it.

What I needed was essentially system('git', ) or `git …`, possibly tied to a specific repository, with a tiny bit of syntactic sugar on top. Using Grit just to throw away 90% of the code, and then add some to deal with the remaining rough edges (or deal with the always–capturing interface), seemed like pushing a square peg into the round hole. Thus, MiniGit was born. 135 lines of code, no dependencies outside of stdlib, tested with MRI 1.8.7 to 2.0.0, REE, JRuby and Rubinius, and providing just the simple layer to comfortably talk with the Git command line. Just what I needed for Vendorificator and a bunch of glue scripts in various places.

Using MiniGit

First, obviously, require 'minigit'.

In the simplest case, you just want to call a Git command in current directory, and let the results flow to the terminal. Then you just call methods on the MiniGit class. They Ruby–style arguments, with hash–based options and everything, are translated into command line options. Of course, you can always provide plain strings and not care about the sugarcoating. If the command returns non–zero status (i.e. fails), MiniGit::GitError is raised.

Underscores are translated to dashes. Some of the Git commands conflict with Ruby’s built-in methods (most notably, git clone). When this is a problem, use the #git method to provide command name as an argument.

If you need to get called command’s standard output, use MiniGit::Capturing class.

MiniGit and MiniGit::Capturing are regular classes, and you can make instances of them. “Normal” instances (created by MiniGit.new or MiniGit::Capturing.new without arguments) aren’t very interesting – you can call methods like on the class itself, and get the same effect. More interesting are bound instances: you can give a path to repository, and the commands called by instance will be always called in this repository’s context. For convenience, #capturing and #noncapturing methods return an instance of MiniGit::Capturing or MiniGit tied to the same repository

Issues and future

I’m figuring out how to correctly handle Git’s pager support; in some cases, I want to disable it completely (like when I want to show output of multiple git diff or git log commands in one sequence). If pager is bothering you, set ENV['GIT_PAGER'] = ''.

I’d like to have a nicer interface to get an array or enumerator of lines instead of a single string, or to strip the trailing newline off the captured output. I didn’t think of a good syntax for that that would be expressive, concise, and not too DWIM.

The current implementation of capturing is the lowest common denominator: Kernel#`.  This means I need to do a roundabout, shell-escape a list of arguments as string, so that it is passed to shell and split again. This is ugly and inefficient. I’d want to be able to use a newer implementation (such as Process.spawn, or the posix-spawn library), but without dropping support for Ruby 1.8 or introducing new dependencies. I’m working on code that would choose the implementation based on current ruby version and availability of recommended libraries – I’d use posix-spawn when it’s available and loaded, then fall back to Process.spawn, and only fall back to Kernel#`/ Kernel#system when nothing better is available. This would also enable user to override the behaviour in subclass, making it trivial to add e.g. MiniGit::Capturing::Lines class that returns array of output lines. I won’t be able to brag about 135 lines anymore, but the library would be more useful and more efficient this way.

If you have any ideas, comments, or just want to tell me why this is a terribly stupid idea – the comment form is down here ☟ and GitHub fork button is over there ☞. Have fun!

One thought on “MiniGit: a simple Ruby interface to the Git command

Comments are closed.