Minority Opinions

Not everyone can be mainstream, after all.

Diagramming Git Commits

leave a comment »

Last week’s problem seems to have been solved, but I would still like a better solution.  It turns out that dot‘s dia format has been left dropped by the developers, waiting for someone to write a plugin for the new architecture.  Perhaps I could do that, if I cared enough.  However, it would probably require many more features than my use case before it could be accepted upstream.  I can get by with something quite a bit simpler.  In this case, I’ve bashed together everything I need into a single script, currently known as git-dot:

#!/bin/sh
# Open the commit graph in Dia.
# Can accept the same arguments as git log;
# try --all and/or --date-order, for example.
# Might not be that pretty, but can be manipulated by hand.

shellcode='''#'
(
  tempdir=~/.cache
  filename="$tempdir/git-log-$$.dia"

  (
    echo 'digraph G {'
    git log --pretty=format:"%h %p%d" $* | perl -lnx "$0"
    echo '}'
  ) | dot -Tplain | python "$0" | gzip > $filename
  dia $filename 2>/dev/null
  rm $filename
) &

exit
'''#"""#'''

#!/usr/bin/env python

import fileinput
from itertools import count

header = r'''<?xml version="1.0" encoding="UTF-8"?>
<dia:diagram xmlns:dia="http://www.lysator.liu.se/~alla/dia/">
  <dia:diagramdata>
    <dia:attribute name="pagebreak">
      <dia:color val="#CCCCFF"/>
    </dia:attribute>
  </dia:diagramdata>
  <dia:layer name="Main" visible="true" active="true">
'''#"""#'''

node = r'''
    <dia:group>
      <dia:object type="Standard - Ellipse" version="0" id="O{nodenum}">
        <dia:attribute name="elem_corner">
          <dia:point val="{tx},{ty}"/>
        </dia:attribute>
        <dia:attribute name="elem_width">
          <dia:real val="{dx}"/>
        </dia:attribute>
        <dia:attribute name="elem_height">
          <dia:real val="{dy}"/>
        </dia:attribute>
      </dia:object>
      <dia:object type="Standard - Text" version="1" id="O{textnum}">
        <dia:attribute name="obj_pos">
          <dia:point val="{cx},{cy}"/>
        </dia:attribute>
        <dia:attribute name="text">
          <dia:composite type="text">
            <dia:attribute name="string">
              <dia:string>#{label}#</dia:string>
            </dia:attribute>
            <dia:attribute name="height">
              <dia:real val="0.8"/>
            </dia:attribute>
            <dia:attribute name="alignment">
              <dia:enum val="1"/>
            </dia:attribute>
          </dia:composite>
        </dia:attribute>
        <dia:attribute name="valign">
          <dia:enum val="2"/>
        </dia:attribute>
        <dia:connections>
          <dia:connection handle="0" to="O{nodenum}" connection="8"/>
        </dia:connections>
      </dia:object>
    </dia:group>
'''#"""#'''

edge = r'''
    <dia:object type="Standard - Line" version="0" id="O{edgenum}">
      <dia:attribute name="end_arrow">
        <dia:enum val="3"/>
      </dia:attribute>
      <dia:connections>
        <dia:connection handle="0" to="O{tailnum}" connection="8"/>
        <dia:connection handle="1" to="O{headnum}" connection="8"/>
      </dia:connections>
    </dia:object>
'''#"""#'''

footer = r'''
  </dia:layer>
</dia:diagram>
'''#"""#'''

scale = 3.6

print header

nodes = {}
nextid = count().next
for line in fileinput.input():
    words = [word.strip('"') for word in line.strip().split()]
    if words[0] == "node":
        name, x, y, width, height, label = words[1:7]
        cx = float(x) * scale
        cy = float(y) * scale
        w = float(width) * scale
        h = float(height) * scale
        nodenum = nextid()
        textnum = nextid()
        nodes[name] = nodenum
        print node.format(
            nodenum = nodenum,
            textnum = textnum,
            label = label.replace("\\n", "&"+"#x0A;"),
            tx = cx - w*.5,
            ty = cy - h*.5,
            cx = cx,
            cy = cy,
            dx = w,
            dy = h,
        )
    elif words[0] == "edge":
        edgenum = nextid()
        tail, head = words[1:3]
        print edge.format(
            edgenum = edgenum,
            headnum = nodes[head],
            tailnum = nodes[tail],
        )

print footer

perlcode=r'''

#!/usr/bin/perl

if (/(\w+)((?: \w+)*)( \(.*\)|)/) {
    my ($name, $label, $parents, $tags) = ($1, $1, $2, $3);
    foreach (split /[ (),]+/, $tags) {
        $label .= "\\n$_" if $_;
    }

    print qq("$name" [label="$label"];);
    foreach (split / /, $parents) {
        print qq("$_"->"$name";) if $_;
    }
}

#'''#"""#'''

Yes, that’s a single file.  In three languages.  With three shebang lines.  Two of which are required.

Basically, it uses perl to parse git log output for a list of commits with their parents and references into a simple dot format.  That gets parsed by dot into a set of positions and sizes for the nodes, which is parsed by python into a simplified dia format, and compressed into a temporary file.  That file is then sent to dia for your perusal, while the main script exits.

There are things here that could be simplified, of course.  The entire perl script, for example, could be written as a command-line argument within the shell portion, but it’s more readable like this.  For that matter, the entire perl and shell logic could be rewritten as python code.  XML isn’t meant to be written by hand like this, particularly in bits and pieces, but it’s a lot more work to do it properly.  It assumes that all nodes are ellipses.  It destroys any work dot has done to route connections around intervening nodes.  It can even make connections so long that dia displays them improperly, probably due to an integer overflow.

That said, it’s a lot of fun to play with the output.  I get to delete nodes and branches that aren’t relevant to the problem at hand.  I get to move things around, and they stay properly connected as long as I’m careful.  Best of all, I get to change the color of each node.

Now, I find myself wishing for a new feature.  I want to mass-select all of a given node’s ancestors, to make them all green.  I want to mass-select all of a different node’s descendants, to make them all red.  For another branch, I want to select just the uncolored ones, between the green base and the red merge point, to make them all blue.  Perhaps I can use Dia’s python console to do that.  Sadly, my first attempt managed to crash the program.  (Hint: Don’t try to unselect an item that’s part of a selected group.)

Advertisements

Written by eswald

10 Apr 2012 at 4:09 pm

Posted in Technology

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s