3 # git log --pretty="%H %P" | this program
5 import sys
, subprocess
, argparse
, pickle
9 Mergepat
= patterns
.patterns
['ExtMerge']
10 IntMerge
= patterns
.patterns
['IntMerge']
11 IntMerge2
= patterns
.patterns
['IntMerge2']
15 def __init__(self
, id, tree
= None):
19 self
.tree
= tree
or '?'
25 def normalize_tree(self
, tree
):
26 if tree
[:6] == 'git://':
28 if tree
.find('git.kernel.org') >= 0:
29 stree
= tree
.split('/')
30 return '$KORG/%s/%s' % (stree
[-2], stree
[-1])
34 command
= ['git', 'log', '-1', self
.id]
35 p
= subprocess
.Popen(command
, cwd
= Repo
, stdout
= subprocess
.PIPE
,
37 for line
in p
.stdout
.readlines():
39 # Maybe it's a merge of an external tree.
41 m
= Mergepat
.search(line
)
43 self
.tree
= self
.normalize_tree(m
.group(3))
47 # Or maybe it's an internal merge.
49 m
= IntMerge
.search(line
) or IntMerge2
.search(line
)
55 def add_commit(self
, id):
56 self
.commits
.append(id)
58 def add_merge(self
, merge
):
59 self
.merges
.append(merge
)
62 # Read the list of commits from the input stream and find which
63 # merge brought in each.
65 def ingest_commits(src
):
67 for line
in src
.readlines():
70 mc
= Mergelist
[find_merge(sline
[0])] # Needs try
71 if len(sline
) > 2: # is a merge
72 mc
.add_merge(Merge(commit
))
77 sys
.stderr
.write('\r%5d ' % (count
))
82 # Figure out which merge brought in a commit.
86 def find_merge(commit
):
87 command
= ['git', 'describe', '--contains', commit
]
88 p
= subprocess
.Popen(command
, cwd
= Repo
, stdout
= subprocess
.PIPE
,
90 desc
= p
.stdout
.readline().decode('utf8')
93 # The description line has the form:
97 # the portion up to the last ^ describes the merge we are after;
98 # in the absence of an ^, assume it's on the main branch.
100 uparrow
= desc
.rfind('^')
104 # OK, now get the real commit ID of the merge. Maybe we have
108 return MergeIDs
[desc
[:uparrow
]]
112 # Nope, we have to dig it out the hard way.
114 command
= ['git', 'log', '--pretty=%H', '-1', desc
[:uparrow
]]
115 p
= subprocess
.Popen(command
, cwd
= Repo
, stdout
= subprocess
.PIPE
,
117 merge
= p
.stdout
.readline().decode('utf8').strip()
119 # If we get back the same commit, we're looking at one of Linus's
120 # version number tags.
124 MergeIDs
[desc
[:uparrow
]] = merge
129 # Internal merges aren't interesting from our point of view. So go through,
130 # find them all, and move any commits from such into the parent.
132 def zorch_internals(merge
):
134 for m
in merge
.merges
:
137 merge
.commits
+= m
.commits
138 new_merges
+= m
.merges
141 merge
.merges
= new_merges
144 # Figure out how many commits flowed at each stage.
146 def count_commits(merge
):
147 merge
.ccount
= len(merge
.commits
) + 1 # +1 to count the merge itself
148 for m
in merge
.merges
:
149 merge
.ccount
+= count_commits(m
)
153 # ...and how many flowed between each pair of trees
157 def tree_stats(merge
):
159 tcount
= Treecounts
[merge
.tree
]
161 tcount
= Treecounts
[merge
.tree
] = { }
162 for m
in merge
.merges
:
163 mcount
= tcount
.get(m
.tree
, 0)
164 tcount
[m
.tree
] = mcount
+ m
.ccount
168 # Maybe we only want so many top-level trees
170 def trim_trees(limit
):
171 srcs
= Treecounts
['mainline']
172 srcnames
= srcs
.keys()
173 srcnames
.sort(lambda t1
, t2
: srcs
[t2
] - srcs
[t1
])
174 nextra
= len(srcnames
) - limit
176 for extra
in srcnames
[limit
:]:
177 zapped
+= srcs
[extra
]
179 srcs
['%d other trees' % (nextra
)] = zapped
181 # Take our map of the commit structure and boil it down to how many commits
182 # moved from one tree to the next.
185 def dumptree(start
, indent
= ''):
189 print '%s%s%s: %d/%d %s' % (indent
, int, start
.id[:10],
190 len(start
.merges
), len(start
.commits
),
192 for merge
in start
.merges
:
193 dumptree(merge
, indent
+ ' ')
195 def dumpflow(tree
, indent
= '', seen
= []):
197 srcs
= Treecounts
[tree
]
200 srctrees
= srcs
.keys()
201 srctrees
.sort(lambda t1
, t2
: srcs
[t2
] - srcs
[t1
])
204 print 'Skip', src
, srcs
[src
], seen
206 print '%s%4d %s' % (indent
, srcs
[src
], src
)
207 dumpflow(src
, indent
= indent
+ ' ', seen
= seen
+ [tree
])
213 graph
= graphviz
.Digraph('mainline', filename
= file, format
= 'png')
214 graph
.body
.extend(['label="Patch flow into the mainline"',
217 graph
.attr('node', fontsize
="20", color
="red", shape
='ellipse')
218 graph
.node('mainline')
219 graph
.attr('node', fontsize
="14", color
="black", shape
='polygon',
221 GV_out_node(graph
, 'mainline')
224 def GV_fixname(name
):
225 return name
.replace(':', '/') # or Graphviz chokes
228 if count
>= RedThresh
:
230 if count
>= YellowThresh
:
234 def GV_out_node(graph
, node
, seen
= []):
236 srcs
= Treecounts
[node
]
237 except KeyError: # "applied by linus"
239 srctrees
= srcs
.keys()
240 srctrees
.sort(lambda t1
, t2
: srcs
[t2
] - srcs
[t1
])
243 graph
.edge(GV_fixname(src
), GV_fixname(node
),
244 taillabel
='%d' % srcs
[src
], labelfontsize
="14",
245 color
= GV_color(srcs
[src
]), penwidth
='2')
246 GV_out_node(graph
, src
, seen
+ [node
])
248 # argument parsing stuff.
251 p
= argparse
.ArgumentParser()
252 p
.add_argument('-d', '--dump', help = 'Dump merge list to file',
253 required
= False, default
= '')
254 p
.add_argument('-g', '--gvoutput', help = 'Graphviz output',
255 required
= False, default
= '')
256 p
.add_argument('-l', '--load', help = 'Load merge list from file',
257 required
= False, default
= '')
258 p
.add_argument('-o', '--output', help = 'Output file',
259 required
= False, default
= '-')
260 p
.add_argument('-r', '--repo', help = 'Repository location',
261 required
= False, default
= '/home/corbet/kernel')
262 p
.add_argument('-t', '--trim', help = 'Trim top level to this many trees',
263 required
= False, default
= 0, type = int)
264 p
.add_argument('-R', '--red', help = 'Red color threshold',
265 required
= False, default
= 800, type = int)
266 p
.add_argument('-Y', '--yellow', help = 'Yellow color threshold',
267 required
= False, default
= 200, type = int)
272 args
= p
.parse_args()
275 YellowThresh
= args
.yellow
280 dumpfile
= open(args
.load
, 'r')
281 Mergelist
= pickle
.loads(dumpfile
.read())
283 Mainline
= Mergelist
['mainline']
285 Mainline
= Merge('mainline', tree
= 'mainline')
286 ingest_commits(sys
.stdin
)
288 dumpfile
= open(args
.dump
, 'w')
289 dumpfile
.write(pickle
.dumps(Mergelist
))
292 # Now generate the flow graph.
295 zorch_internals(Mainline
)
297 Treecounts
['mainline'] = { 'Applied by Linus': len(Mainline
.commits
) }
298 print 'total commits', count_commits(Mainline
)
301 trim_trees(args
.trim
)
305 GV_out(args
.gvoutput
)