Skip to content

Instantly share code, notes, and snippets.

@multiscan
Forked from cl4u2/git-log-to-tikz.rb
Last active October 25, 2020 19:45
Show Gist options
  • Save multiscan/d0f0b35b79c4a8228ca0efc0e5f59421 to your computer and use it in GitHub Desktop.
Save multiscan/d0f0b35b79c4a8228ca0efc0e5f59421 to your computer and use it in GitHub Desktop.
Extract git history to tikz picture - complete latex document and minimal graph
#!/usr/bin/env ruby
# https://gist.github.com/multiscan/d0f0b35b79c4a8228ca0efc0e5f59421
# A small ruby script to extract a git history to a tikz picture
# Author: Michael Hauspie <Michael.Hauspie@lifl.fr>
# Author: Lennart C. Karssen <lennart@karssen.org>
# Author: Claudio Pisa <claudio.pisa@uniroma2.it>
# Author: Giovanni Cangiani <giovanni.cangiani@epfl.ch>
#
# Not clean code, not always working well, but does its job in most of
# the cases I needed :)
#
# LCK: Added some ideas from this tex.stackexchange answer:
# http://tex.stackexchange.com/a/156501/7221
# CP: Create a complete LaTeX doc and show only a minimal graph
# GC: Replace single tikz picture with tabular as suggested
# here: https://latex.org/forum/viewtopic.php?t=28162
require 'optparse'
# A commit object
class Commit
attr_accessor :hash
attr_accessor :children
attr_accessor :parents
attr_accessor :message
attr_reader :node_pos
attr_reader :node_color
# Construct a commit from a line
# A line is commit_hash [child1_hash ... childN_hash] message
def initialize()
@hash = nil
@children = Hash.new()
@parents = Hash.new()
@message = ""
@node_pos = 0
@message_pos = 0
# These colours require \usepackage[dvipsnames]{xcolor}
@colours = ["ForestGreen", "Dandelion", "Red", "cyan", "magenta", "orange"]
end
def build(line)
# Parse each word to match a hash or the commmit message
pos=0
line.split(" ").each do |word|
if word =~ /[a-f0-9]{7}/ && @message == "" # match a short sha-1 commit tag
if ! @hash
@hash = word
else
@parents[word] = nil
end
elsif word == '*' # Position of the node
@node_pos = pos
@node_color = @colours[pos]
elsif word =~ /[^|\/\\]/
@message_pos = pos
@message << " #{word}"
end
pos = pos + 1
end
@message.delete!("!")
@message.lstrip!()
if !@hash
return false
end
return true
end
# sets a child object
def set_parent(c)
@parents[c.hash] = c
c.children[@hash] = self
end
def export_to_tikz(ypos)
puts "\\node[commit, #{node_color}, fill=#{node_color}] (#{@hash}) at (#{0.5*@node_pos},#{ypos}) {};"
puts "\\node[right,xshift=10] (label_#{@hash}) at (#{@hash}.east) {\\verb!#{@hash}: #{@message}!};"
@children.each_value do |child|
puts "\\path[#{node_color}] (#{@hash}) to[out=90,in=-90] (#{child.hash});"
end
end
def export_to_tabular1()
puts "{\\tt #{@hash}} & \\hspace{#{2*@node_pos}em}\\pos{#{@hash}}{#{node_color}} & Author & #{@message} \\\\"
end
def export_to_tabular2()
@children.each_value do |child|
if @node_pos > child.node_pos
puts "\\path[#{node_color},draw,thick] (#{@hash}) to[out=90,in=-90] (#{child.hash});"
elsif @node_pos < child.node_pos
puts "\\path[#{child.node_color},draw,thick] (#{@hash}) to[out=60,in=-90] (#{child.hash});"
else
puts "\\path[#{node_color},draw,thick] (#{@hash}) to[out=90,in=-90] (#{child.hash});"
end
end
end
def to_s()
"#{@hash}: #{@message} #{@node_pos} #{@message_pos}"
end
end
class Branch
attr_accessor :name
attr_accessor :hash
attr_accessor :commit
def initialize(line)
words = line.split(" ")
@name = words[0]
@hash = words[1]
@commit = nil
end
end
# A repository, which is a collection of commit objects and branches
class Repository
def initialize()
@commits = Hash.new()
@branches = Array.new()
end
def add_commit(commit)
@commits[commit.hash] = commit
end
def add_branch(branch)
if ! @commits.has_key?(branch.hash)
return false
end
c = @commits.fetch(branch.hash)
branch.commit = c
@branches << branch
return true
end
# This iterates other commits and resolves its parents
def resolve_parents()
@commits.each_value do |commit|
commit.parents.keys.each do |parent_hash|
c = @commits.fetch(parent_hash)
commit.set_parent(c) # Link the commit object to its parent
end
end
end
def export_to_tikz
puts "\\documentclass[a4paper]{article}"
puts "\\usepackage[dvipsnames]{xcolor}"
puts "\\usepackage{listings}"
puts "\\usepackage{tikz}"
puts "\\usetikzlibrary{arrows, automata, backgrounds, calendar, chains, matrix, mindmap, patterns, petri, shadows, shapes.geometric, shapes.misc, spy, trees}"
puts "\\begin{document}"
puts "\\begin{tikzpicture}"
puts "\\tikzstyle{commit}=[draw,circle,fill=white,inner sep=0pt,minimum size=5pt]"
puts "\\tikzstyle{every path}=[draw]"
puts "\\tikzstyle{branch}=[draw,rectangle,rounded corners=3,fill=white,inner sep=2pt,minimum size=5pt]"
ypos=0
ystep=-0.5
@commits.each_value do |commit|
commit.export_to_tikz(ypos)
ypos = ypos + ystep
end
@branches.each do |branch|
puts "\\node[branch,right,xshift=10] (#{branch.name}) at (label_#{branch.hash}.east) {\\lstinline{#{branch.name}}};"
end
puts "\\end{tikzpicture}"
puts "\\end{document}"
end
# \newcommand{\pos}[2]{\tikz[overlay,remember picture,baseline=(#1.base)] \node (#1) {\point{#2}};}
# \tikzstyle{commit}=[draw,circle,fill=white,inner sep=0pt,minimum size=5pt]
# \tikzstyle{every path}=[draw]
# \tikzstyle{branch}=[draw,rectangle,rounded corners=3,fill=white,inner sep=2pt,minimum size=5pt]
def export_to_tabular
puts '
\documentclass[a4paper]{article}
\usepackage[dvipsnames]{xcolor}
\usepackage{listings}
\usepackage{tikz}
\usetikzlibrary{arrows, automata, backgrounds, calendar, chains, matrix, mindmap, patterns, petri, positioning, shadows, shapes.geometric, shapes.misc, spy, trees}
\newcommand*{\point}[1]{\textcolor{#1}{$\bullet$}}
\newcommand{\pos}[2]{\tikz[overlay,remember picture] \node (#1) {\point{#2}};}
\usepackage{array}
\usepackage{tabularx}
\usepackage{microtype}
\renewcommand{\baselinestretch}{1.5}
\begin{document}
\centering
\begin{tabularx}{\textwidth}{rclX}
'
@commits.each_value do |commit|
commit.export_to_tabular1()
end
puts '\end{tabularx}'
puts '\tikz[overlay,remember picture] {'
@commits.each_value do |commit|
commit.export_to_tabular2()
end
puts '}'
# @branches.each do |branch|
# # puts "\\node[branch,right,xshift=10] (#{branch.name}) at (label_#{branch.hash}.east) {\\lstinline{#{branch.name}}};"
# puts "\\node[branch,right,xshift=10] (#{branch.name}) at (#{branch.hash}.west) {\\lstinline{#{branch.name}}};"
# end
# puts "\\end{tikzpicture}"
puts '\end{document}'
end
end
r = Repository.new()
# Start parsing command line options
# This hash will hold the options parsed from the command line by
# OptionParser.
options = {}
optparse = OptionParser.new do|opts|
# Explain usage of this script
opts.banner =
"Usage: git-log-to-tikz.rb [options]
This script has to be run in a Git repository.
"
# Define the options and some explanation
options[:tophash] = "HEAD"
options[:simplify] = "--simplify-by-decoration"
options[:format] = 'tikz'
opts.on( '-t', '--tophash HASH',
'Hash of the top-most commit to include') do |h|
options[:tophash] = h
end
opts.on( '-F', '--nosimplify',
'Simplified (fewer commits visible) git output') do |h|
options[:simplify] = ''
end
opts.on( '-g', '--tabular',
'Use tabular format instead of single tikz picture') do |h|
options[:format] = 'tabular'
end
end
# Parse the command-line. The 'parse!' method parses ARGV and removes
# any options found there, as well as any parameters for the options.
optparse.parse!
# Extract commits
# simplify="--simplify-by-decoration"
simplify=""
if options[:tophash] != "HEAD"
# If a top hash was entered, don't use the --branches option or it
# will output all commits.
# Note: When using -t, the labels for branches are not added.
cmd = "git log " +
options[:tophash] +
" --graph --oneline --parents #{options[:simplify]}"
else
cmd = "git log --branches --graph --oneline --parents #{options[:simplify]}"
end
`#{cmd}`.lines().each do |line|
c = Commit.new()
if c.build(line)
r.add_commit(c)
end
end
r.resolve_parents()
# Extract branches
`git branch -av | cut -b 3-`.lines().each do |line|
r.add_branch(Branch.new(line))
end
if options[:format] == 'tikz'
r.export_to_tikz()
else
r.export_to_tabular()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment