kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
9.5 KiB
278 lines
9.5 KiB
#!/usr/bin/python
|
|
#emacs: -*- mode: python-mode; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: t -*-
|
|
#ex: set sts=4 ts=4 sw=4 noet:
|
|
#------------------------- =+- Python script -+= -------------------------
|
|
"""
|
|
@file graphviz_plot.py
|
|
@date Mon Aug 11 11:54:34 2008
|
|
@brief
|
|
|
|
|
|
Yaroslav Halchenko CS@UNM, CS@NJIT
|
|
web: http://www.onerussian.com & PSYCH@RUTGERS
|
|
e-mail: yoh@onerussian.com ICQ#: 60653192
|
|
|
|
DESCRIPTION (NOTES): Prototype code to draw using graphviz a git
|
|
history of merges with collapses, where plain 1-parent 1-child
|
|
commits occured
|
|
|
|
TODO:
|
|
use pydot internally to create a graph
|
|
proper filenames specification
|
|
....
|
|
|
|
EXAMPLE USAGE:
|
|
|
|
../graphviz_plot.py --all # will generate graph.dot
|
|
dot -Tsvg graph.dot >| graph.svg
|
|
inkscape graph.svg
|
|
|
|
COPYRIGHT: Yaroslav Halchenko 2008
|
|
|
|
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 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 St, Fifth Floor, Boston,
|
|
MA 02110-1301, USA.
|
|
|
|
On Debian system see /usr/share/common-licenses/GPL for the full license.
|
|
"""
|
|
#-----------------\____________________________________/------------------
|
|
|
|
__author__ = 'Yaroslav Halhenko'
|
|
__revision__ = '$Revision: $'
|
|
__date__ = '$Date: $'
|
|
__copyright__ = 'Copyright (c) 2008 Yaroslav Halchenko'
|
|
__license__ = 'GPL'
|
|
|
|
|
|
import sys, re, os
|
|
|
|
gitargs = ' '.join(sys.argv[1:])
|
|
|
|
# lets read all commits and place into list
|
|
commits = []
|
|
|
|
split_strip = lambda f, split_symbol: [[x.strip() for x in l.split(split_symbol)]
|
|
for l in f.readlines()]
|
|
|
|
print "I: Reading the commits"
|
|
f = os.popen('git rev-list --pretty=format:"%%H|%%h|%%an|%%s" "%s" | grep -v "^commit"'
|
|
% gitargs)
|
|
commits = split_strip(f, '|')
|
|
|
|
print "I: Reading the tree"
|
|
f = os.popen('git rev-list --parents "%s"' % gitargs)
|
|
tree_down = split_strip(f, ' ')
|
|
|
|
|
|
print "I: Reading heads"
|
|
f = os.popen("git branch --no-color -v --no-abbrev | sed -e 's,\*,,g' | awk '{print $1, $2;}'")
|
|
heads_list = split_strip(f, ' ')
|
|
heads_dict = dict( [ (y,x) for x,y in heads_list ] )
|
|
|
|
|
|
# need to traverse the tree and create double linked nodes instead of
|
|
# just parent since we would need to remove some
|
|
tree = dict([(n[0], [n[1:], [], [], [], []]) for n in tree_down])
|
|
|
|
print "I: Rebuilding childrens"
|
|
# lets rebuild children nodes -- may be there is cleaner way? :-)
|
|
for id_, (parents, children, indirect_parents, cherried_into, cherried_from) in tree.iteritems():
|
|
for parent in parents:
|
|
tree[parent][1].append(id_)
|
|
|
|
print "I: Figuring out cherry picks"
|
|
# following evil bash line gives what is needed
|
|
# git rev-list --no-merges --pretty=format:"|%H|%h|%ci|%an|%ae|%s|%b|" --all | grep -v '^commit' | tr '&' '%' | tr '\n' '&' | sed -e 's/|&/|\n/g' -e 's/&|/|/g' | sort -t\| -k 5,8 | uniq -d -s75 -D | while read commit; do sha=$(echo $commit | awk -F\| '{print $2;}'); diffsha=$(git diff ${sha}^..${sha} | grep '^[+-]' | sha1sum | awk '{print $1;}'); echo "$commit|$diffsha"; done | sort -t\| -k 5,10 | uniq -d -s75 -D
|
|
#
|
|
# in summary it does
|
|
# 1. list all non merge commits and looks for similar according
|
|
# to author/commit_msg
|
|
#
|
|
# 2. since some times commit_msg's are bogus -- we also add sha1sum for
|
|
# changed context by that commit -- lines which start with + or -
|
|
#
|
|
# 3. and then once again - sort and uniq ;-)
|
|
#
|
|
# result looks like
|
|
# |072c0f7a0449130e7363be9ef618668147ece87e|072c0f7|2008-07-30 18:26:26 +0200|Julian_chu|julian_chu@openmoko.com|[splinter] Bump up the reversion of splinter to 507|For some UI tunning stuff||66e879ca796685319d4602487e0e52c629217191
|
|
# |23ed84fc72097e7abe1dac21453a33ce9440991d|23ed84f|2008-07-30 23:01:44 +0800|Julian_chu|julian_chu@openmoko.com|[splinter] Bump up the reversion of splinter to 507|For some UI tunning stuff||66e879ca796685319d4602487e0e52c629217191
|
|
# |8c1e9ae76bf39c141d2f5bdc759c5fb628574463|8c1e9ae|2008-05-11 23:32:35 +0000||julian_chu@openmoko.com|Add Ninja Theme as default|||8f42ba37c6fef619bf922a689d560e0ffc979b5f
|
|
# |ac9d16ad220468ba1f359355e155e2dc2a232272|ac9d16a|2008-05-25 22:27:39 +0800||julian_chu@openmoko.com|Add Ninja Theme as default|||8f42ba37c6fef619bf922a689d560e0ffc979b5f
|
|
#
|
|
# so we just need to compare the times to derive from where it was cherry picked
|
|
cmd = 'git rev-list --no-merges --pretty=format:"|%%H|%%h|%%ci|%%an|%%ae|%%s|%%b|" %s | grep -v "^commit" | tr "&" "%%" | tr "\\n" "&" | sed -e "s/|&/|\\n/g" -e "s/&|/|/g" | sort -t\| -k 5,8 | uniq -d -s75 -D | while read commit; do sha=$(echo $commit | awk -F\| "{print \$2;}"); diffsha=$(git diff ${sha}^..${sha} | grep "^[+-]" | sha1sum | awk "{print \$1;}"); echo "$commit|$diffsha"; done | sort -t\| -k 5,10 | uniq -d -s75 -D' % gitargs
|
|
f = os.popen(cmd)
|
|
cherry_picks_raw = split_strip(f, '|')
|
|
#print cherry_picks_raw
|
|
|
|
print "I: Analyzing cherry picks"
|
|
# 3 - date
|
|
# >=4 -- use for id
|
|
cp_uid_last = None
|
|
cp_date_min = 'X'
|
|
cherry_picks = {}
|
|
cp_ids = []
|
|
for cp in cherry_picks_raw + [ [' ']*12 ]: # trailer for easier logic
|
|
id_ = cp[1]
|
|
cp_date = cp[3]
|
|
cp_uid = ' '.join(cp[4:])
|
|
if cp_uid_last != cp_uid and cp_uid_last is not None:
|
|
# process it since we got to a new one
|
|
#print "D: Process cherry pick '%s' created in %s" % (cp_uid_last, id_min)
|
|
try:
|
|
cp_ids.pop(cp_ids.index(id_min))
|
|
except:
|
|
# XXX should not happen but does -- figure out later
|
|
pass
|
|
tree[id_min][3] = cp_ids
|
|
for cp_ in cp_ids:
|
|
tree[cp_][4] = [id_min]
|
|
cp_ids = []
|
|
pass
|
|
if cp_date < cp_date_min:
|
|
cp_date_min = cp_date
|
|
id_min = id_
|
|
cp_uid_last = cp_uid
|
|
cp_ids.append(id_)
|
|
assert(id_ == ' ')
|
|
|
|
|
|
print "I: Figuring out branches ownership from merge messages"
|
|
# for now each node belongs just to 1 branch although sure thing it
|
|
# could be in few branches at that moment
|
|
regexp = re.compile("Merge (?P<type>branch|commit) '(?P<branch0>.*)'(?: into (?P<branch1>[^. ,|]*))?")
|
|
branches = {}
|
|
for commit in commits:
|
|
fid = commit[0]
|
|
# only those which are left
|
|
if not fid in tree:
|
|
continue
|
|
comment = commit[3]
|
|
regres = regexp.search(comment)
|
|
if regres is None:
|
|
# print "I: don't know branch for %s" % fid
|
|
continue
|
|
|
|
regdict = regres.groupdict()
|
|
|
|
if regdict['branch1'] == None:
|
|
regdict['branch1'] = 'master'
|
|
|
|
branches[fid] = regdict['branch1']
|
|
# TODO check what is message with octopus
|
|
if regdict['type'] == 'branch':
|
|
#import pydb
|
|
#pydb.debugger()
|
|
branches[ tree[fid][0][1] ] = regdict['branch0']
|
|
branches[ tree[fid][0][0] ] = regdict['branch1']
|
|
|
|
from sets import Set
|
|
branches_names = list(Set(branches.values() + heads_dict.values()))
|
|
print "D: Detected branches from merges: ", branches_names
|
|
branches_colors = dict([[x, i+2] for i,x in enumerate(branches_names)])
|
|
#print branches_colors
|
|
|
|
#for k, p in tree.iteritems():
|
|
# print k, p
|
|
|
|
print "I: Pruning"
|
|
# lets go through all nodes and if there is only 1 parent and 1 child -- KILL!!!
|
|
for id_ in tree.keys():
|
|
parents, children, indirect_parents, cherried_into, cherried_from = tree[id_]
|
|
if len(children) == 1 and len(parents) + len(indirect_parents) == 1 \
|
|
and len(cherried_into) + len(cherried_from) == 0 \
|
|
and (not id_ in heads_dict):# and False:
|
|
# print "D: %s with %s" % (id_, tree[id_])
|
|
tree.pop(id_)
|
|
|
|
# update children
|
|
for child in children:
|
|
tchild = tree[child]
|
|
tchild[2] += parents + indirect_parents
|
|
for i in [0,2]:
|
|
try:
|
|
tchild[i].pop(tchild[i].index(id_))
|
|
#print "removed %s from %s's parent %d" % (id_, child, i)
|
|
except:
|
|
pass
|
|
|
|
# update the parent
|
|
for parent in parents + indirect_parents:
|
|
tparent = tree[parent]
|
|
tparent[1] += children
|
|
try:
|
|
tparent[1].pop(tparent[1].index(id_))
|
|
#print "removed %s from %s's children" % (id_, parent)
|
|
except:
|
|
pass
|
|
|
|
|
|
#print tree
|
|
print "I: Storing graph"
|
|
# plot the damn graph ;-)
|
|
fout = open('graph.dot', 'w')
|
|
fout.write("digraph lattice {\n")
|
|
|
|
# print nodes
|
|
for commit in commits:
|
|
fid = commit[0]
|
|
# only those which are left
|
|
if not fid in tree:
|
|
continue
|
|
|
|
sid = commit[1]
|
|
if len(commit)>2:
|
|
comment = ' '.join(commit[3:])
|
|
else:
|
|
comment = ''
|
|
|
|
# condition comment
|
|
comment = comment.replace('"', "'")[:60]
|
|
|
|
try:
|
|
color = ", fillcolor=\"/brbg11/%s\", style=\"filled\"" % branches_colors[branches[fid]]
|
|
except:
|
|
color = ""
|
|
|
|
fout.write('n%s [ label="%s\\n%s"%s]\n' % (fid, sid, comment, color))
|
|
|
|
# print heads
|
|
for head_id, head_name in heads_dict.iteritems():
|
|
try:
|
|
color = ", color=\"/brbg11/%s\", style=\"filled\"" % branches_colors[head_name]
|
|
except:
|
|
color = ""
|
|
fout.write('h%s [ shape=box, label="%s"%s]\n' % (head_id, head_name, color))
|
|
|
|
# print edges
|
|
for id_, (parents, children, indirect_parents, cherried_into, cherried_from) in tree.iteritems():
|
|
c = ("", " [ color=\"red\"]")[int(len(parents)>1)]
|
|
|
|
for p in parents:
|
|
fout.write("n%s -> n%s%s\n" % (id_, p, c))
|
|
|
|
for p in indirect_parents:
|
|
fout.write("n%s -> n%s [style=dashed, color=\"red\"]\n" % (id_, p))
|
|
|
|
if len(cherried_from)>0:
|
|
fout.write("n%s -> n%s [style=dashed]\n" % (id_, cherried_from[0]))
|
|
|
|
# print links from heads
|
|
for head_id, head_name in heads_dict.iteritems():
|
|
fout.write("h%s -> n%s\n" % (head_id, head_id))
|
|
|
|
fout.write("}\n")
|
|
fout.close()
|
|
|