Skip to content

Instantly share code, notes, and snippets.

@ben
Last active August 2, 2026 16:57
Show Gist options
  • Select an option

  • Save ben/cd1fd2c291766cd7eb3df4f4eb09047f to your computer and use it in GitHub Desktop.

Select an option

Save ben/cd1fd2c291766cd7eb3df4f4eb09047f to your computer and use it in GitHub Desktop.
Pro Git: text-sourced commit-diagram toolchain research (not for merging)

Text-sourced commit diagrams for Pro Git — toolchain research (NOT a proposal to merge)

Status: exploratory research, not a decision. Nothing in progit/progit3 is changed. This gist exists so the findings and prototype renders are reviewable outside a chat transcript.

Repo state referenced below: progit/progit3@c9850b7

Caveat: Source Code Pro isn't installed on the machine that produced these renders, so every image below — including the "original" renders — fell back to a substitute font. Treat all shape/layout/color comparisons as valid; don't judge letterforms.


The ask

We want images to be text-source-controlled instead of Sketch binaries, while keeping style control and the ability to generate the commit diagrams used everywhere in the book.

What's actually in images/

  • 113 SVGs, all exported from diagram-source/progit.sketch (Sketch 56.3). 112 have a paired 800px-wide PNG — that PNG is what the book actually image::-references; the SVG is already unused by the build.
  • One font (Source Code Pro), six colors: #efefe7 commit box, #f44d27 ref box, #8f8981 line/arrow, #f0f0f0/gray text, #00909a teal (object-model figures).
  • Box sizes repeat hard: 89×33 (521 uses), 106×33 refs (119 uses), 86×33 (63 uses) — a de facto symbol library, not free-form art.
  • Node positions are hand-nudged, not on a grid — confirmed by extracting every box's translate() across all 113 files. So there's no mechanical SVG→source conversion; every diagram is a redraw, regardless of tool.
  • ~101 of 113 are commit-graph shaped (DAGs: commits, branches, merges, rebases). The other 12 are bespoke figures with custom block-arrow art: areas, lifecycle, centralized, distributed, local, clean, smudge, reset-workflow, benevolent-dictator, integration-manager, centralized_workflow, symbols.
  • Nothing in Rakefile touches images/*.svg; the site build (site/scripts/build-book.mjs) just copies images/ verbatim. Changing how diagrams are produced touches zero build wiring today.

The number that actually motivates this

103 of 113 diagram SVGs contain the literal string master. book_master_to_main_inventory.md already flags that renaming mastermain in the book's running examples requires regenerating the diagrams that show it. Today that's ~103 trips through Sketch. Text sources turn that into sed + rebuild.


Prototype 1 (recommended): Graphviz + a shared theme

For the ~101 DAG-shaped diagrams. Rebuilt two real diagrams from this book against the actual palette/font/box-size constants above.

two-branches.svg — source: example-graphviz-two-branches.dot

Original (Sketch/SVG) vs. Graphviz render:

original two-branches graphviz two-branches

basic-merging-2.svg (two-lane merge topology — the hard case) — source: example-graphviz-merge.dot

Original vs. Graphviz render:

original merge diagram graphviz merge diagram

Two gotchas hit and solved during prototyping:

  • rank=same fights rankdir=LR. It pins nodes into columns, not rows, which rotated the first merge attempt into a vertical mess. Fix: use group= per lane instead of rank=same.
  • Graphviz has no native #include. Two working fixes confirmed: pipe through cpp -P -traditional-cpp file.dot.in | dot -Tsvg, or theme entirely via -N/-E/-G CLI flags. Either gives one central style file, satisfying the "control style" requirement.

Why this is the pick: zero new concepts for book contributors, packaged everywhere (brew/apt install graphviz), no license, and a DAG layout engine is the correct tool for Git DAGs. The master→main rename becomes mechanical.

Prototype 2: D2

Source: example-d2-two-branches.d2

d2 two-branches

Nicer style-control primitive — real named classes (classes: { commit: {...} } ) instead of a preprocessor hack. But: laid out right-to-left against the intended arrow direction on the first try, 13.5 KB output vs. Graphviz's 3.4 KB for the same 5 nodes, and it's a newer single-vendor dependency most contributors won't already have. Worth it only if the class system matters more than ubiquity.

Prototype 3: small in-repo generator (JSON spec → SVG)

Source: example-generator-render.py + example-generator-spec.json — ~60 lines, no layout engine, explicit lane/column placement using the book's actual box-size and grid-pitch constants.

generator two-branches

Closest visual match of the three since it hard-codes the book's exact geometry rather than delegating to a layout algorithm. 1.8 KB output. Cost: you own the renderer, and it doesn't help with the 12 bespoke figures either — those need real drawing primitives (block arrows, swimlanes), not DAG layout.

The 12 bespoke figures — not addressed by any of the above

Example, areas.svg (custom block-arrow art, not a DAG):

areas bespoke figure

And data-model-1.svg (this one is DAG-shaped and would port fine to Graphviz):

data model diagram


Recommendation

Graphviz for the ~101 DAGs; leave the 12 bespoke figures in Sketch. They're stable, rarely edited, and most don't say "master" — porting them by hand buys little and chasing 100% coverage is where a project like this stalls.

Open questions before anyone builds this for real:

  1. Font packaging. Source Code Pro needs to be vendored or pinned in CI, or renders vary by machine — a real reproducibility bug for a book (see caveat at top).
  2. Do rendered PNGs stay committed, or generate at build time? The book only ever references .png. Generating at build time is cleaner but adds graphviz + a rasterizer (e.g. rsvg-convert) to CI.

No repo changes were made. All files here are throwaway prototypes for evaluation only.

direction: right
classes: {
commit: {
style: { fill: "#efefe7"; stroke: "#efefe7"; font-color: gray; font-size: 12; border-radius: 0 }
width: 86
height: 33
}
ref: {
style: { fill: "#f44d27"; stroke: "#f44d27"; font-color: "#f0f0f0"; font-size: 12; bold: true }
width: 106
height: 33
}
}
c1: 98ca9 { class: commit }
c2: 34ac2 { class: commit }
c3: f30ab { class: commit }
main: main { class: ref }
testing: testing { class: ref }
c2 -> c1: { style.stroke: "#8f8981" }
c3 -> c2: { style.stroke: "#8f8981" }
main -> c3: { style.stroke: "#8f8981" }
testing -> c3: { style.stroke: "#8f8981" }
#!/usr/bin/env python3
"""Toy prototype: declarative commit-graph spec -> SVG in the book's house style."""
import json,sys
C=dict(commit="#efefe7",commit_fg="gray",ref="#f44d27",ref_fg="#f0f0f0",line="#8f8981")
BW,BH,GX,GY=86,33,145,61 # box + grid pitch, taken from the real SVGs
def box(x,y,label,kind,w=None):
w=w or (106 if kind=="ref" else BW)
fill=C["ref"] if kind=="ref" else C["commit"]
fg=C["ref_fg"] if kind=="ref" else C["commit_fg"]
return (f'<g transform="translate({x},{y})">'
f'<rect width="{w}" height="{BH}" style="fill:{fill}"/>'
f'<text x="{w/2}" y="21" style="fill:{fg};font-family:\'Source Code Pro\';'
f'font-size:12px;font-weight:700;text-anchor:middle">{label}</text></g>')
def arrow(x1,y1,x2,y2):
return (f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
f'style="stroke:{C["line"]};stroke-width:1.5" marker-end="url(#a)"/>')
spec=json.load(open(sys.argv[1]))
nodes,out={},[]
for n in spec["nodes"]:
x,y=4+n["col"]*GX, 4+n["row"]*GY
w=106 if n["kind"]=="ref" else BW
nodes[n["id"]]=(x,y,w)
out.append(box(x,y,n["label"],n["kind"],w))
for a,b in spec["edges"]:
(x1,y1,w1),(x2,y2,w2)=nodes[a],nodes[b]
if y1==y2:
out.append(arrow(x1, y1+BH/2, x2+w2+6 if x2<x1 else x2-6, y2+BH/2) if x2<x1
else arrow(x1+w1, y1+BH/2, x2-6, y2+BH/2))
else:
out.append(arrow(x1+w1/2, y1 if y2<y1 else y1+BH, x2+w2/2, y2+BH+6 if y2<y1 else y2-6))
W=max(x+w for x,_,w in nodes.values())+8
H=max(y for _,y,_ in nodes.values())+BH+8
print(f'<?xml version="1.0" encoding="UTF-8"?>\n<svg viewBox="0 0 {W} {H}" xmlns="http://www.w3.org/2000/svg">'
f'<defs><marker id="a" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto">'
f'<path d="M0,0 L9,3.5 L0,7 z" fill="{C["line"]}"/></marker></defs>'
+ "".join(out) + "</svg>")
{"nodes":[{"id":"c1","label":"98ca9","col":0,"row":1,"kind":"commit"},
{"id":"c2","label":"34ac2","col":1,"row":1,"kind":"commit"},
{"id":"c3","label":"f30ab","col":2,"row":1,"kind":"commit"},
{"id":"main","label":"main","col":2,"row":0,"kind":"ref"},
{"id":"test","label":"testing","col":2,"row":2,"kind":"ref"}],
"edges":[["c2","c1"],["c3","c2"],["main","c3"],["test","c3"]]}
digraph g {
rankdir=LR; bgcolor="none"; splines=line; ranksep=0.32; nodesep=0.30;
node [shape=box, style="filled,rounded", fontname="Source Code Pro", fontsize=12,
fillcolor="#efefe7", color="#efefe7", fontcolor="gray",
width=1.05, height=0.42, fixedsize=true];
edge [color="#8f8981", arrowsize=0.7, dir=back];
// mainline lane
C0->C1->C2 [dir=back];
C2->C4->C6 [dir=back];
C0[group=main]; C1[group=main]; C2[group=main]; C4[group=main]; C6[group=main];
// topic lane, one rank lower via an invisible spacer chain
C3[group=topic]; C5[group=topic];
C2->C3 [dir=back]; C3->C5 [dir=back];
C5->C6 [dir=forward];
node [fillcolor="#f44d27", color="#f44d27", fontcolor="#f0f0f0", style="filled", width=0.95];
main[group=main]; iss53[group=topic];
C6->main [dir=forward];
C5->iss53 [dir=forward];
}
digraph g {
rankdir=LR; bgcolor="none"; splines=false;
node [shape=box, style=filled, fontname="Source Code Pro", fontsize=12,
fillcolor="#efefe7", color="#efefe7", fontcolor="gray",
width=1.05, height=0.42, fixedsize=true];
edge [color="#8f8981", arrowsize=0.7, dir=back];
"98ca9" -> "34ac2" -> "f30ab" [dir=back];
node [fillcolor="#f44d27", color="#f44d27", fontcolor="#f0f0f0", fontname="Source Code Pro Bold"];
main; testing;
{ rank=same; "f30ab"; }
"f30ab" -> main [dir=forward];
"f30ab" -> testing [dir=forward];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment