clj.orcery

Language, Expression and Design

Saturday

07

March 2015

gita - the introspective jgit wrapper

by Chris Zheng,

I'm stoked to announce gita - a new clojure wrapper for jgit. It looks somewhat like the shell version in that there is only a single top-level function (git).

(use 'gita.core)

(git)
;  Subtasks for git are:
;
;  [:add :apply :archive :blame :branch :checkout 
;   :cherry :clean :clone :commit :describe :diff 
;   :fetch :gc :init :log :ls :merge :name :notes 
;   :pull :push :rebase :reflog :reset :revert :rm 
;   :stash :status :submodule :tag]
;

There's documentation already on the github page already on workflows for creating a repo, committing files and branching and there could be more but I'm a basic git user and so I don't know how all the other commands work. So I'm hoping someone reading this will feel enough compassion to do at least a :rebase workflow in the readme.

I know enough git to get by so the main point for writing the library was not about getting into the internals of how git works. My goal was motivated by laziness, or rather:

How I can use information that an existing library gives me to design a better api.

In a sense, this was an experiment in reflective meta-programming.

I've been thinking about ways and techniques to wrap big libraries using clojure for a long time so I'm very happy with the current technique. To put things in context, the jgit project is huge; just the org.eclipse.jgit folder alone contains 161744 lines in all its *.java files as of today, March 7 2015. The gita code, which wraps up most of the top level stuff is currently less than 600 lines. It's also very readable (in my opinion anyways).

The secret sauce is reflection, as well as some macro magic. I wasn't too worried about reflection being slow because only the top level functions needed to be wrapped. Anyways, I figured no one was going to run (git :commit) a million times in real life. My main concern was to make the library look and feel like git because a wrapper should make the underlying library that it's wrapping more usable and more transparent. My aim was to return the relevant data to the user in the clojure-ish way - that is, to convert everything into a clojure data-structure. It's a generic approach that I'm hoping to abstract out... but that's another day. For now ladies and gents, I present to you gita:

(git :init :directory "/tmp/gita-example")
=> "/tmp/gita-example/.git"

;; Lets check the status of our new repository
(git "/tmp/gita-example" :status)
=> {:clean? true, :uncommitted-changes? false}

;; We add a file
(spit "/tmp/gita-example/hello.txt" "hello there")
(git :status)
=> {:clean? false, :uncommitted-changes? false, :untracked #{"hello.txt"}}

;; And do the add/commit dance
(git :add :filepattern ["hello.txt"])
=> {"hello.txt" #{:merged}}

(git :commit :message "Added Hello.txt")
=> {:commit-time 1425683330,
    :name "9f1177ad928d7dea2afedd58b1fb7192b3523a6c",
    :author-ident {:email-address "",
                   :name "Chris Zheng",
                   :time-zone-offset 330,
                   :when #inst "2015-03-06T23:08:50.000-00:00"},
    :full-message "Added Hello.txt"}

;; I love logs
(git :log)
=> [{:commit-time 1425683330,
    :name "9f1177ad928d7dea2afedd58b1fb7192b3523a6c",
    :author-ident {:email-address "",
                   :name "Chris Zheng",
                   :time-zone-offset 330,
                   :when #inst "2015-03-06T23:08:50.000-00:00"},
     :full-message "Added Hello.txt"}]