#+TITLE: Drawing Git Graphs with Graphviz and Org-Mode
#+AUTHOR: Correl Roush
#+STARTUP: indent inlineimages showall hideblocks
#+OPTIONS: toc:nil num:nil
#+PROPERTY: header-args :exports both :results silent
#+KEYWORDS: emacs org-mode git graphviz
#+begin_src emacs-lisp :exports results :results silent
(defun vector-image (name)
(let ((basename (concat (file-name-base buffer-file-name) "-" name)))
(cond ((eq org-export-current-backend 'latex)
(concat basename ".eps"))
(t (concat basename ".svg")))))
#+end_src
#+name: inline-image
#+begin_src emacs-lisp :exports none :var name="example"
(if (org-export-derived-backend-p org-export-current-backend 'html)
(concat "#+BEGIN_HTML\n"
(let ((image-file (vector-image name)))
(with-temp-buffer
(insert-file-contents image-file)
(buffer-string)))
"#+END_HTML\n")
(concat "[[file:" (vector-image name) "]]\n"))
#+end_src
#+BEGIN_HTML
#+END_HTML
Digging through Derek Feichtinger's [[https://github.com/dfeich/org-babel-examples][org-babel examples]] (which I came
across via [[http://irreal.org/blog/?p%3D4162][irreal.org]]), I found he had some great examples of
displaying git-style graphs using graphviz. I thought it'd be a fun
exercise to generate my own graphs based on his graphviz source using
elisp, and point it at actual git repos.
* Getting Started
I started out with the goal of building a simple graph showing a
mainline branch and a topic branch forked from it and eventually
merged back in.
Using Derek's example as a template, I described 5 commits on a master
branch, plus two on a topic branch.
#+begin_src dot :file (vector-image "graph-example")
digraph G {
rankdir="LR";
bgcolor="transparent";
node[width=0.15, height=0.15, shape=point];
edge[weight=2, arrowhead=none];
node[group=master];
1 -> 2 -> 3 -> 4 -> 5;
node[group=branch];
2 -> 6 -> 7 -> 4;
}
#+end_src
The resulting image looks like this:
#+CALL: inline-image(name="graph-example") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-graph-example.svg]]
** Designing the Data Structure
The first thing I needed to do was describe my data structure. Leaning
on my experiences reading and working through [[https://www.google.com/url?sa%3Dt&rct%3Dj&q%3D&esrc%3Ds&source%3Dweb&cd%3D1&cad%3Drja&uact%3D8&ved%3D0CB8QFjAA&url%3Dhttps%253A%252F%252Fmitpress.mit.edu%252Fsicp%252F&ei%3DlH6gVau5OIGR-AG8j7yACQ&usg%3DAFQjCNHTCXQK7qN-kYibdy_MqRBWxlr8og&sig2%3DLu9WIhyuTJS92e8hxne0Aw&bvm%3Dbv.97653015,d.cWw][SICP]], I got to work
building a constructor function, and several accessors.
I decided to represent each node on a graph with an id, a list of
parent ids, and a group which will correspond to the branch on the
graph the commit belongs to.
#+begin_src emacs-lisp
(defun git-graph/make-node (id &optional parents group)
(list id parents group))
(defun git-graph/node-id (node)
(nth 0 node))
(defun git-graph/node-parents (node)
(nth 1 node))
(defun git-graph/node-group (node)
(nth 2 node))
#+end_src
** Converting the structure to Graphviz
Now that I had my data structures sorted out, it was time to step
through them and generate the graphviz source that'd give me the
nice-looking graphs I was after.
The graph is constructed using the example above as a template. The
nodes are defined first, followed by the edges between them.
#+name: git-graph/to-graphviz
#+begin_src emacs-lisp
(defun git-graph/to-graphviz (id nodes)
(string-join
(list
(concat "digraph " id " {")
"bgcolor=\"transparent\";"
"rankdir=\"LR\";"
"node[width=0.15,height=0.15,shape=point,fontsize=8.0];"
"edge[weight=2,arrowhead=none];"
(string-join
(-map #'git-graph/to-graphviz-node nodes)
"\n")
(string-join
(-uniq (-flatten (-map
(lambda (node) (git-graph/to-graphviz-edges node nodes))
nodes)))
"\n")
"}")
"\n"))
#+end_src
For the sake of readability, I'll format the output:
#+name: git-graph/to-graphviz
#+begin_src emacs-lisp
(defun git-graph/to-graphviz-pretty (id nodes)
(with-temp-buffer
(graphviz-dot-mode)
(insert (git-graph/to-graphviz id nodes))
(indent-region (point-min) (point-max))
(buffer-string)))
#+end_src
Each node is built, setting its group attribute when applicable.
#+begin_src emacs-lisp
(defun git-graph/to-graphviz-node (node)
(let ((node-id (git-graph/to-graphviz-node-id
(git-graph/node-id node))))
(concat node-id
(--if-let (git-graph/node-group node)
(concat "[group=\"" it "\"]"))
";")))
#+end_src
Graphviz node identifiers are quoted to avoid running into issues with
spaces or other special characters.
#+name: git-graph/to-graphviz-nodes
#+begin_src emacs-lisp
(defun git-graph/to-graphviz-node-id (id)
(format "\"%s\"" id))
#+end_src
For each node, an edge is built connecting the node to each of its
parents.
#+name: git-graph/to-graphviz-edges
#+begin_src emacs-lisp
(defun git-graph/to-graphviz-edges (node &optional nodelist)
(let ((node-id (git-graph/node-id node))
(parents (git-graph/node-parents node))
(node-ids (-map 'git-graph/node-id nodelist)))
(-map (lambda (parent)
(unless (and nodelist (not (member parent node-ids)))
(git-graph/to-graphviz-edge node-id parent)))
parents)))
(defun git-graph/to-graphviz-edge (from to)
(concat
(git-graph/to-graphviz-node-id to)
" -> "
(git-graph/to-graphviz-node-id from)
";"))
#+end_src
With that done, the simple graph above could be generated with the
following code:
#+name: git-example
#+begin_src emacs-lisp :exports code :results silent
(git-graph/to-graphviz-pretty
"example"
(list (git-graph/make-node 1 nil "master")
(git-graph/make-node 2 '(1) "master")
(git-graph/make-node 3 '(2) "master")
(git-graph/make-node 4 '(3 7) "master")
(git-graph/make-node 5 '(4) "master")
(git-graph/make-node 6 '(2) "branch")
(git-graph/make-node 7 '(6) "branch")))
#+end_src
Which generates the following graphviz source:
#+begin_src dot :noweb yes :file (vector-image "generated-git-example")
<>
#+end_src
The generated image matches the example exactly:
#+CALL: inline-image(name="generated-git-example") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-generated-git-example.svg]]
* Adding Labels
The next thing my graph needed was a way of labeling nodes. Rather
than trying to figure out some way of attaching a separate label to a
node, I decided to simply draw a labeled node as a box with text.
#+begin_src dot :file (vector-image "graph-labels")
digraph G {
rankdir="LR";
bgcolor="transparent";
node[width=0.15, height=0.15, shape=point,fontsize=8.0];
edge[weight=2, arrowhead=none];
node[group=main];
1 -> 2 -> 3 -> 4 -> 5;
5[shape=box,label=master];
node[group=branch1];
2 -> 6 -> 7 -> 4;
7[shape=box,label=branch];
}
#+end_src
#+CALL: inline-image(name="graph-labels") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-graph-labels.svg]]
** Updating the Data Structure
I updated my data structure to support an optional label applied to a
node. I opted to store it in an associative list alongside the group.
#+name: git-graph/structure
#+begin_src emacs-lisp
(defun git-graph/make-node (id &optional parents options)
(list id parents options))
(defun git-graph/node-id (node)
(nth 0 node))
(defun git-graph/node-parents (node)
(nth 1 node))
(defun git-graph/node-group (node)
(cdr (assoc 'group (nth 2 node))))
(defun git-graph/node-label (node)
(cdr (assoc 'label (nth 2 node))))
#+end_src
** Updating the Graphviz node generation
The next step was updating the Graphviz generation functions to handle
the new data structure, and set the shape and label attributes of
labeled nodes.
#+name: git-graph/to-graphviz-nodes
#+begin_src emacs-lisp
(defun git-graph/to-graphviz-node (node)
(let ((node-id (git-graph/to-graphviz-node-id (git-graph/node-id node))))
(concat node-id
(git-graph/to-graphviz-node--attributes node)
";")))
(defun git-graph/to-graphviz-node--attributes (node)
(let ((attributes (git-graph/to-graphviz-node--compute-attributes node)))
(and attributes
(concat "["
(mapconcat (lambda (pair)
(format "%s=\"%s\""
(car pair) (cdr pair)))
attributes
", ")
"]"))))
(defun git-graph/to-graphviz-node--compute-attributes (node)
(-filter #'identity
(append (and (git-graph/node-group node)
(list (cons 'group (git-graph/node-group node))))
(and (git-graph/node-label node)
(list (cons 'shape 'box)
(cons 'label (git-graph/node-label node)))))))
#+end_src
I could then label the tips of each branch:
#+name: graph-example-labels
#+begin_src emacs-lisp :exports code :results silent
(git-graph/to-graphviz-pretty
"labeled"
(list (git-graph/make-node 1 nil '((group . "master")))
(git-graph/make-node 2 '(1) '((group . "master")))
(git-graph/make-node 3 '(2) '((group . "master")))
(git-graph/make-node 4 '(3 7) '((group . "master")))
(git-graph/make-node 5 '(4) '((group . "master")
(label . "master")))
(git-graph/make-node 6 '(2) '((group . "branch")))
(git-graph/make-node 7 '(6) '((group . "branch")
(label . "branch")))))
#+end_src
#+begin_src dot :file (vector-image "graph-labels-generated") :noweb yes
<>
#+end_src
#+CALL: inline-image(name="graph-labels-generated") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-graph-labels-generated.svg]]
* Automatic Grouping Using Leaf Nodes
Manually assigning groups to each node is tedious, and easy to
accidentally get wrong. Also, with the goal to graph git repositories,
I was going to have to figure out groupings automatically anyway.
To do this, it made sense to traverse the nodes in [[https://en.wikipedia.org/wiki/Topological_sorting][topological order]].
Repeating the example above,
#+begin_src dot :file (vector-image "graph-topo")
digraph G {
rankdir="LR";
bgcolor="transparent";
node[width=0.15, height=0.15, shape=circle];
edge[weight=2, arrowhead=none];
node[group=main];
1 -> 2 -> 3 -> 4 -> 5;
node[group=branch1];
2 -> 6 -> 7 -> 4;
}
#+end_src
#+CALL: inline-image(name="graph-topo") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-graph-topo.svg]]
These nodes can be represented (right to left) in topological order as
either ~5, 4, 3, 7, 6, 2, 1~ or ~5, 4, 7, 6, 3, 2, 1~.
Having no further children, ~5~ is a leaf node, and can be used as a
group. All first parents of ~5~ can therefore be considered to be in
group ~5~.
~7~ is a second parent to ~4~, and so should be used as the group for
all of its parents not present in group ~5~.
#+name: git-graph/group-topo
#+begin_src emacs-lisp
(defun git-graph/group-topo (nodelist)
(reverse
(car
(-reduce-from
(lambda (acc node)
(let* ((grouped-nodes (car acc))
(group-stack (cdr acc))
(node-id (git-graph/node-id node))
(group-from-stack (--if-let (assoc node-id group-stack)
(cdr it)))
(group (or group-from-stack node-id))
(parents (git-graph/node-parents node))
(first-parent (first parents)))
(if group-from-stack
(pop group-stack))
(if (and first-parent (not (assoc first-parent group-stack)))
(push (cons first-parent group) group-stack))
(cons (cons (git-graph/make-node node-id
parents
`((group . ,group)
(label . ,(git-graph/node-label node))))
grouped-nodes)
group-stack)))
nil
nodelist))))
#+end_src
While iterating through the node list, I maintained a stack of pairs
built from the first parent of the current node, and the current
group. To determine the group, the head of the stack is checked to see
if it contains a group for the current node id. If it does, that group
is used and it is popped off the stack, otherwise the current node id
is used.
The following table illustrates how the stack is used to store and
assign group relationships as the process iterates through the node
list:
#+caption: Progressing through the nodes
| Node | Parents | Group Stack | Group |
|------+---------+-----------------+-------|
| 5 | (4) | (4 . 5) | 5 |
| 4 | (3 7) | (3 . 5) | 5 |
| 3 | (2) | (2 . 5) | 5 |
| 7 | (6) | (6 . 7) (2 . 5) | 7 |
| 6 | (2) | (2 . 5) | 7 |
| 2 | (1) | (1 . 5) | 5 |
| 1 | | | 5 |
** Graph without automatic grouping
#+name: graph-no-auto-grouping
#+begin_src emacs-lisp :exports code :results silent
(git-graph/to-graphviz-pretty
"nogroups"
(list (git-graph/make-node 5 '(4) '((label . master)))
(git-graph/make-node 4 '(3 7))
(git-graph/make-node 3 '(2))
(git-graph/make-node 7 '(6) '((label . develop)))
(git-graph/make-node 6 '(2))
(git-graph/make-node 2 '(1))
(git-graph/make-node 1 nil)))
#+end_src
#+begin_src dot :noweb yes :file (vector-image "graph-no-auto-grouping")
<>
#+end_src
#+CALL: inline-image(name="graph-no-auto-grouping") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-graph-no-auto-grouping.svg]]
** Graph with automatic grouping
#+name: graph-with-auto-grouping
#+begin_src emacs-lisp :exports code :results silent
(git-graph/to-graphviz-pretty
"autogroups"
(git-graph/group-topo
(list (git-graph/make-node 5 '(4) '((label . master)))
(git-graph/make-node 4 '(3 7))
(git-graph/make-node 3 '(2))
(git-graph/make-node 7 '(6) '((label . develop)))
(git-graph/make-node 6 '(2))
(git-graph/make-node 2 '(1))
(git-graph/make-node 1 nil))))
#+end_src
#+begin_src dot :noweb yes :file (vector-image "graph-with-auto-grouping")
<>
#+end_src
#+CALL: inline-image(name="graph-with-auto-grouping") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-graph-with-auto-grouping.svg]]
* Graphing a Git Repository
Satisfied that I had all the necessary tools to start graphing real
git repositories, I created an example repository to test against.
** Creating a Sample Repository
Using the following script, I created a sample repository to test
against. I performed the following actions:
- Forked a develop branch from master.
- Forked a feature branch from develop, with two commits.
- Added another commit to develop.
- Forked a second feature branch from develop, with two commits.
- Merged the second feature branch to develop.
- Merged develop to master and tagged it.
#+begin_src sh :exports results :results silent
rm -rf /tmp/test.git
#+end_src
#+begin_src sh :exports both :results silent
mkdir /tmp/test.git
cd /tmp/test.git
git init
touch README
git add README
git commit -m 'initial'
git commit --allow-empty -m 'first'
git checkout -b develop
git commit --allow-empty -m 'second'
git checkout -b feature-1
git commit --allow-empty -m 'feature 1'
git commit --allow-empty -m 'feature 1 again'
git checkout develop
git commit --allow-empty -m 'third'
git checkout -b feature-2
git commit --allow-empty -m 'feature 2'
git commit --allow-empty -m 'feature 2 again'
git checkout develop
git merge --no-ff feature-2
git checkout master
git merge --no-ff develop
git tag -a 1.0 -m '1.0!'
#+end_src
** Generating a Graph From a Git Branch
The first order of business was to have a way to call out to git and
return the results:
#+name: git-graph/from-git
#+begin_src emacs-lisp
(defun git-graph/git-execute (repo-url command &rest args)
(with-temp-buffer
(shell-command (format "git -C \"%s\" %s"
repo-url
(string-join (cons command args)
" "))
t)
(buffer-string)))
#+end_src
Next, I needed to get the list of commits for a branch in topological
order, with a list of parent commits for each. It turns out git
provides exactly that via its =rev-list= command.
#+name: git-graph/from-git
#+begin_src emacs-lisp
(defun git-graph/git-rev-list (repo-url head)
(-map (lambda (line) (split-string line))
(split-string (git-graph/git-execute
repo-url
"rev-list" "--topo-order" "--parents" head)
"\n" t)))
#+end_src
I also wanted to label branch heads wherever possible. To do this, I
looked up the revision name from git, discarding it if it was relative
to some other named commit.
#+name: git-graph/from-git
#+begin_src emacs-lisp
(defun git-graph/git-label (repo-url rev)
(let ((name (string-trim
(git-graph/git-execute repo-url
"name-rev" "--name-only" rev))))
(unless (s-contains? "~" name)
name)))
#+end_src
Generating the graph for a single branch was as simple as iterating
over each commit and creating a node for it.
#+begin_src emacs-lisp
(defun git-graph/git-graph-head (repo-url head)
(git-graph/group-topo
(-map (lambda (rev-with-parents)
(let* ((rev (car rev-with-parents))
(parents (cdr rev-with-parents))
(label (git-graph/git-label repo-url rev)))
(git-graph/make-node rev parents
`((label . ,label)))))
(git-graph/git-rev-list repo-url head))))
#+end_src
Here's the result of graphing the =master= branch:
#+name: graph-git-branch
#+begin_src emacs-lisp
(git-graph/to-graphviz-pretty
"git"
(git-graph/git-graph-head
"/tmp/test.git"
"master"))
#+end_src
#+begin_src dot :file (vector-image "git-graph-branch") :noweb yes
<>
#+end_src
#+CALL: inline-image(name="git-graph-branch") :exports results :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-git-graph-branch.svg]]
** Graphing Multiple Branches
To graph multiple branches, I needed a function for combining
histories. To do so, I simply append any nodes I don't already know
about in the first history from the second.
#+name: git-graph/adder
#+begin_src emacs-lisp
(defun git-graph/+ (a b)
(append a
(-remove (lambda (node)
(assoc (git-graph/node-id node) a))
b)))
#+end_src
From there, all that remained was to accumulate the branch histories
and output the complete graph:
#+name: git-graph/from-git
#+begin_src emacs-lisp
(defun git-graph/git-load (repo-url heads)
(-reduce #'git-graph/+
(-map (lambda (head)
(git-graph/git-graph-head repo-url head))
heads)))
#+end_src
And here's the example repository, graphed in full:
#+name: graph-git-repo
#+begin_src emacs-lisp
(git-graph/to-graphviz-pretty
"git"
(git-graph/git-load
"/tmp/test.git"
'("master" "feature-1")))
#+end_src
#+begin_src dot :file (vector-image "git-graph-repo") :noweb yes
<>
#+end_src
#+CALL: inline-image(name="git-graph-repo") :results raw replace
#+RESULTS:
[[file:2015-07-12-git-graphs-git-graph-repo.svg]]
* Things I may add in the future
** Limiting Commits to Graph
Running this against repos with any substantial history can make the
graph unwieldy. It'd be a good idea to abstract out the commit list
fetching, and modify it to support different ways of limiting the
history to display.
Ideas would include:
- Specifying commit ranges
- Stopping at a common ancestor to all graphed branches (e.g., using
=git-merge-base=).
- Other git commit limiting options, like searches, showing only merge
or non-merge commits, etc.
** Collapsing History
Another means of reducing the size of the resulting graph would be to
collapse unimportant sections of it. It should be possible to collapse
a section of the graph, showing a count of skipped nodes.
The difficult part would be determining what parts aren't worth
drawing. Something like this would be handy, though, for concisely
graphing the state of multiple ongoing development branches (say, to
get a picture of what's been going on since the last release, and
what's still incomplete).
#+begin_src dot :file (vector-image "git-graph-long")
digraph G {
rankdir="LR";
bgcolor="transparent";
node[width=0.15,height=0.15,shape=point];
edge[weight=2,arrowhead=none];
node[group=main];
1 -> 2 -> 3 -> 4 -> 5;
node[group=branch];
2 -> 6 -> 7 -> 8 -> 9 -> 10 -> 4;
}
#+end_src
#+CALL: inline-image(name="git-graph-long") :results raw replace
#+caption: A graph with multiple nodes on a branch.
#+RESULTS:
[[file:2015-07-12-git-graphs-git-graph-long.svg]]
#+begin_src dot :file (vector-image "git-graph-collapsed")
digraph G {
rankdir="LR";
bgcolor="transparent";
node[width=0.15,height=0.15,shape=point];
edge[weight=2,arrowhead=none];
node[group=main];
1 -> 2 -> 3 -> 4 -> 5;
node[group=branch];
2 -> 6;
6 -> 10[style=dashed,label="+3"];
10 -> 4;
}
#+end_src
#+CALL: inline-image(name="git-graph-collapsed") :results raw replace
#+caption: The same graph, collapsed.
#+RESULTS:
[[file:2015-07-12-git-graphs-git-graph-collapsed.svg]]
** Clean up and optimize the code a bit
Some parts of this (particularly, the grouping) are probably pretty
inefficient. If this turns out to actually be useful, I may take
another crack at it.
* Final Code
In case anyone would like to use this code for anything, or maybe just
pick it apart and play around with it, all the Emacs Lisp code in this
post is collected into a single file below:
#+begin_src emacs-lisp :noweb yes :exports code :tangle "../files/git-graph.el"
;;; git-graph.el --- Generate git-style graphs using graphviz
;; Copyright (c) 2015 Correl Roush
;;; License:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;;; Code:
(require 'dash)
<>
<>
<>
<>
<>
<>
(provide 'git-graph)
;;; git-graph.el ends here
#+end_src
Download: [[file:/files/git-graph.el]]