Commits

Michele Bini committed 9b1983e

Initial commit

Comments (0)

Files changed (1)

+#!/usr/bin/ruby
+
+# git-digraph - Visualize graphs of Git history, richly annotated with
+# commit information.
+
+# Software required: Ruby, Ruby/Git, Graphviz, Eye-of-Gnome
+
+# Copyright (c) 2012 Michele Bini <michele.bini@gmail.com>
+
+# 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 2 of the License, 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 this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+require 'git'
+require 'time'
+
+class ClassyCLI # http://gitorious.org/classycli
+  # Simple-but-classy command-line interface in 20 lines of code
+
+  # Copyright (c) 2012 Michele Bini <michele.bini@gmail.com>
+
+  # Available under the terms of:
+  # * the MIT License
+  # * the Creative Commons Attribution License CC-by
+
+  def wrong_option (opt)
+    abort "Unrecognized option: " + opt
+  end
+  def process
+    while !ARGV.empty?
+      opt = ARGV.first
+      return ARGV.shift if opt == '--'
+      return unless opt.match(/^-/)
+      mn = opt.gsub(/-/, "_")
+      return self.wrong_option(opt) unless self.respond_to?(mn)
+      m = self.method(mn)
+      a = m.arity
+      ARGV.shift
+      abort "Method #{mn} has wrong number of arguments: #{a}" if 1 < a
+      0 == a ? m.call() : m.call(ARGV.shift)
+    end
+  end
+end
+
+class Options < ClassyCLI
+  def _h
+    self.__help
+  end
+  def __help
+    puts "git digraph OPTIONS* OBJECT*"
+    puts
+    puts "Display directed graph of Git commits"
+    puts
+    puts "Options:"
+    puts "  --graphviz  Raw graphviz output, no display"
+    # puts "  --svg       SVG output"
+    puts "  -a, --all   Include all local branches"
+    puts "  -h, --help  Display this help screen"
+    exit
+  end
+  def __verbose
+    $verbose = true
+  end
+  def __graphviz
+    $raw_graphviz = true
+  end
+  def __word(x)
+    $stderr.puts("Printing a word") if $verbose
+    puts(x)
+  end
+end
+
+Options.new.process
+
+module TimeUnits
+  Second = 1
+  Minute = Second * 60
+  Hour = Minute * 60
+  Day = Hour * 24
+  Week = Day * 7
+  Year = 31556926
+  Month = Year / 12
+  Century = Year * 100
+  Units = TimeUnits.constants.sort do |a, b|
+    TimeUnits.const_get(a) <=> TimeUnits.const_get(b)
+  end
+  def self.approximate(d)
+    u = Units.first;
+    Units.each do |c|
+      return u if TimeUnits.const_get(c)*5 > d
+      u = c
+    end
+  end
+end
+
+def time_ago(t)
+  t = t.round
+  u = TimeUnits.approximate(t)
+  d = (t / TimeUnits.const_get(u.capitalize)).round
+  u = u.to_s.downcase + (d > 1 ? "s" : "")
+  "#{d} #{u} ago"
+end
+
+def rendertxt(x) x.sub(/^ +/, '').gsub(/\n */, ' ').gsub(/"/, '\"').sub(/(.{160}[^ ]*).*/, '\1').gsub(/(.{20}[^ ]*) +/, "\\1\\n").sub(/(\\n)+$/, '') end
+def renderopt(x) (x.map {|k,v| k+"="+v }).join(',') end
+
+g = Git.bare(`git rev-parse --git-dir`.chomp)
+
+@branches = { }
+
+g.branches.local.each do |b|
+  x = @branches[b.gcommit.sha] ||= []
+  x.push(b.name)
+end
+
+@head = g.gcommit('HEAD').sha
+
+unless $raw_graphviz
+  pipe = IO.popen("(dot -T svg >/tmp/git-graph.svg && eog /tmp/git-graph.svg; rm /tmp/git-graph.svg)", "w")
+  $stdout.reopen(pipe)
+end
+
+puts "digraph git {bgcolor=blue color=white"
+
+@commits = {}
+@links = {}
+
+@processcommit = Proc.new do |commit|
+  sha = commit.sha
+  next if @commits.member?(sha)
+  @commits[sha] = nil
+  q = "\""
+  nl = "\\n"
+  au = commit.author.name
+  name = sha[0, 7]
+  m = rendertxt(commit.message)
+  d = time_ago(Time.now - commit.author_date)
+  nodeopts = { }
+  nodeopts['shape']='box'
+  nodeopts['style']='filled'
+  nodeopts['fillcolor']='"#ffbbbb"'
+  firstlinechanged = false
+  if (sha && @branches.member?(sha))
+    nodeopts['shape']='oval'
+    nodeopts['fillcolor']='"#ffffbb"'
+    name = @branches[sha].join(', ') + ", " + name
+    firstlinechanged = true
+  end
+  if (sha == @head)
+    nodeopts['shape']='hexagon'
+    nodeopts['fillcolor']='"#bbffbb"'
+    name = "HEAD, " + name
+    firstlinechanged = true
+  end
+  name = rendertxt(name) if firstlinechanged
+  nodeopts['label'] = q + name + nl + m + nl + au + " " + d + q
+  puts "\"#{sha}\" [#{renderopt(nodeopts)}]"
+  c = q+commit.sha+q
+  last = true
+  commit.parents.each do |p|
+    psha = p.sha
+    lsha = sha + psha
+    next if @links.member?(lsha)
+    last = false
+    @links[lsha] = nil
+    d = commit.diff(p)
+    edgeopts = { }
+    edgeopts['color'] = '"white"'
+    edgeopts['fontcolor'] = '"white"'
+    if (d.size > 1)
+      d = sprintf("%d files\\n%d new lines\\n%d deleted", d.size, d.insertions, d.deletions);
+    elsif (d.size == 1)
+      d = sprintf("%s\\n%d new lines\\n%d deleted", d.first.path, d.insertions, d.deletions);      
+    else
+      d = "No\\nchanges";
+    end
+    edgeopts['label'] = q + d + q
+    puts q+p.sha+q + " -> " + c + " [#{renderopt(edgeopts)}]"
+  end
+  !last
+end
+
+def processlog(x)
+  (x.log(nil)).each do |commit|
+    break unless @processcommit.call(commit)
+  end
+end
+
+# def processdeep(depth)
+#   while (depth > 0) && !@toprocess.empty?
+#     @processcommit.shift.call(commit)
+#     depth--
+#   end
+# end
+
+if (ARGV.empty?)
+  processlog(g)
+else
+  ARGV.each do |o|
+    processlog(g.gcommit(o, depth))
+  end
+end
+
+puts "}"
+
+# $stdout.close
+# Process.wait pipe.pid