3 # git log --pretty="%H %P" | this program
4 # See option descriptions at bottom
6 # This little program cranks through a series of patches, trying to determine
7 # which trees each flowed through on its way to the mainline. It does a
8 # 'git describe' on each, so don't expect it to be fast for large numbers
11 # One warning: it is easily confused by local branches, tags, etc. For
12 # best results, run it on a mainline tree with no added frobs. Using
13 # "git clone --reference" is a relatively easy way to come up with such
14 # a tree without redownloading the whole mess.
16 import sys
, subprocess
, argparse
, pickle
20 Mergepat
= patterns
.patterns
['ExtMerge']
21 IntMerge
= patterns
.patterns
['IntMerge']
22 IntMerge2
= patterns
.patterns
['IntMerge2']
26 def __init__(self
, id, tree
= None):
30 self
.tree
= tree
or '?'
36 def normalize_tree(self
, tree
):
37 if tree
[:6] == 'git://':
39 if tree
.find('git.kernel.org') >= 0:
40 stree
= tree
.split('/')
41 return '$KORG/%s/%s' % (stree
[-2], stree
[-1])
45 command
= ['git', 'log', '-1', self
.id]
46 p
= subprocess
.Popen(command
, cwd
= Repo
, stdout
= subprocess
.PIPE
,
48 for line
in p
.stdout
.readlines():
50 # Maybe it's a merge of an external tree.
52 m
= Mergepat
.search(line
)
54 self
.tree
= self
.normalize_tree(m
.group(3))
58 # Or maybe it's an internal merge.
60 m
= IntMerge
.search(line
) or IntMerge2
.search(line
)
66 def add_commit(self
, id):
67 self
.commits
.append(id)
69 def add_merge(self
, merge
):
70 self
.merges
.append(merge
)
73 # Read the list of commits from the input stream and find which
74 # merge brought in each.
76 def ingest_commits(src
):
78 for line
in src
.readlines():
81 mc
= Mergelist
[find_merge(sline
[0])] # Needs try
82 if len(sline
) > 2: # is a merge
83 mc
.add_merge(Merge(commit
))
88 sys
.stderr
.write('\r%5d ' % (count
))
93 # Figure out which merge brought in a commit.
97 def find_merge(commit
):
98 command
= ['git', 'describe', '--contains', commit
]
99 p
= subprocess
.Popen(command
, cwd
= Repo
, stdout
= subprocess
.PIPE
,
101 desc
= p
.stdout
.readline().decode('utf8')
104 # The description line has the form:
108 # the portion up to the last ^ describes the merge we are after;
109 # in the absence of an ^, assume it's on the main branch.
111 uparrow
= desc
.rfind('^')
115 # OK, now get the real commit ID of the merge. Maybe we have
119 return MergeIDs
[desc
[:uparrow
]]
123 # Nope, we have to dig it out the hard way.
125 command
= ['git', 'log', '--pretty=%H', '-1', desc
[:uparrow
]]
126 p
= subprocess
.Popen(command
, cwd
= Repo
, stdout
= subprocess
.PIPE
,
128 merge
= p
.stdout
.readline().decode('utf8').strip()
130 # If we get back the same commit, we're looking at one of Linus's
131 # version number tags.
135 MergeIDs
[desc
[:uparrow
]] = merge
140 # Internal merges aren't interesting from our point of view. So go through,
141 # find them all, and move any commits from such into the parent.
143 def zorch_internals(merge
):
145 for m
in merge
.merges
:
148 merge
.commits
+= m
.commits
149 new_merges
+= m
.merges
152 merge
.merges
= new_merges
155 # Figure out how many commits flowed at each stage.
157 def count_commits(merge
):
158 merge
.ccount
= len(merge
.commits
) + 1 # +1 to count the merge itself
159 for m
in merge
.merges
:
160 merge
.ccount
+= count_commits(m
)
164 # ...and how many flowed between each pair of trees
168 def tree_stats(merge
):
170 tcount
= Treecounts
[merge
.tree
]
172 tcount
= Treecounts
[merge
.tree
] = { }
173 for m
in merge
.merges
:
174 mcount
= tcount
.get(m
.tree
, 0)
175 tcount
[m
.tree
] = mcount
+ m
.ccount
179 # Maybe we only want so many top-level trees
181 def trim_trees(limit
):
182 srcs
= Treecounts
['mainline']
183 srcnames
= srcs
.keys()
184 srcnames
.sort(lambda t1
, t2
: srcs
[t2
] - srcs
[t1
])
185 nextra
= len(srcnames
) - limit
187 for extra
in srcnames
[limit
:]:
188 zapped
+= srcs
[extra
]
190 srcs
['%d other trees' % (nextra
)] = zapped
192 # Take our map of the commit structure and boil it down to how many commits
193 # moved from one tree to the next.
196 def dumptree(start
, indent
= ''):
200 print '%s%s%s: %d/%d %s' % (indent
, int, start
.id[:10],
201 len(start
.merges
), len(start
.commits
),
203 for merge
in start
.merges
:
204 dumptree(merge
, indent
+ ' ')
206 def dumpflow(tree
, indent
= '', seen
= []):
208 srcs
= Treecounts
[tree
]
211 srctrees
= srcs
.keys()
212 srctrees
.sort(lambda t1
, t2
: srcs
[t2
] - srcs
[t1
])
215 print 'Skip', src
, srcs
[src
], seen
217 print '%s%4d %s' % (indent
, srcs
[src
], src
)
218 dumpflow(src
, indent
= indent
+ ' ', seen
= seen
+ [tree
])
224 graph
= graphviz
.Digraph('mainline', filename
= file, format
= 'png')
225 graph
.body
.extend(['label="Patch flow into the mainline"',
228 graph
.attr('node', fontsize
="20", color
="red", shape
='ellipse')
229 graph
.node('mainline')
230 graph
.attr('node', fontsize
="14", color
="black", shape
='polygon',
232 GV_out_node(graph
, 'mainline')
235 def GV_fixname(name
):
236 return name
.replace(':', '/') # or Graphviz chokes
239 if count
>= RedThresh
:
241 if count
>= YellowThresh
:
245 def GV_out_node(graph
, node
, seen
= []):
247 srcs
= Treecounts
[node
]
248 except KeyError: # "applied by linus"
250 srctrees
= srcs
.keys()
251 srctrees
.sort(lambda t1
, t2
: srcs
[t2
] - srcs
[t1
])
254 graph
.edge(GV_fixname(src
), GV_fixname(node
),
255 taillabel
='%d' % srcs
[src
], labelfontsize
="14",
256 color
= GV_color(srcs
[src
]), penwidth
='2')
257 GV_out_node(graph
, src
, seen
+ [node
])
259 # argument parsing stuff.
262 p
= argparse
.ArgumentParser()
263 p
.add_argument('-d', '--dump', help = 'Dump merge list to file',
264 required
= False, default
= '')
265 p
.add_argument('-g', '--gvoutput', help = 'Graphviz output',
266 required
= False, default
= '')
267 p
.add_argument('-l', '--load', help = 'Load merge list from file',
268 required
= False, default
= '')
269 p
.add_argument('-o', '--output', help = 'Output file',
270 required
= False, default
= '-')
271 p
.add_argument('-r', '--repo', help = 'Repository location',
272 required
= False, default
= '/home/corbet/kernel')
273 p
.add_argument('-t', '--trim', help = 'Trim top level to this many trees',
274 required
= False, default
= 0, type = int)
275 p
.add_argument('-R', '--red', help = 'Red color threshold',
276 required
= False, default
= 800, type = int)
277 p
.add_argument('-Y', '--yellow', help = 'Yellow color threshold',
278 required
= False, default
= 200, type = int)
283 args
= p
.parse_args()
286 YellowThresh
= args
.yellow
291 dumpfile
= open(args
.load
, 'r')
292 Mergelist
= pickle
.loads(dumpfile
.read())
294 Mainline
= Mergelist
['mainline']
296 Mainline
= Merge('mainline', tree
= 'mainline')
297 ingest_commits(sys
.stdin
)
299 dumpfile
= open(args
.dump
, 'w')
300 dumpfile
.write(pickle
.dumps(Mergelist
))
303 # Now generate the flow graph.
306 zorch_internals(Mainline
)
308 Treecounts
['mainline'] = { 'Applied by Linus': len(Mainline
.commits
) }
309 print 'total commits', count_commits(Mainline
)
312 trim_trees(args
.trim
)
316 GV_out(args
.gvoutput
)