gitstats: Refactored bug.py to use a Memory and Options class
[git-stats.git] / src / git_stats / branch.py
blobddf865c519790f626df7550766a6deb28a4d1c10
1 #!/usr/bin/env python
3 import os
4 import sys
6 from optparse import OptionParser
7 from git import Repo
9 from git_stats import parse
11 def prettyName(branch, pretty_names={}):
12 """Looks up the name of a branch and returns it
14 Returns only the last part of the branchname.
16 Args:
17 branch: The branch to print
18 pretty_names: A dictionary with commits and their names
19 """
21 if not pretty_names.has_key(branch):
22 git = Repo(".").git
23 name = git.describe("--all", branch)
24 splitname = name.split('/')
25 name = splitname[-1]
26 pretty_names[branch] = name
27 else:
28 name = pretty_names[branch]
30 return name
32 class Metric:
33 """This class represents a metric
35 It is meant to store arbitrary data related to a specific metric.
36 """
38 def __init__(self):
39 self.branch = ""
40 self.commit = ""
41 self.dilution = ""
43 def __str__(self):
44 res = "Branch %s, dilution: %d." % \
45 (prettyName(self.branch), self.dilution)
47 return res
49 def _bestMetric(metrics):
50 """Finds the best metric from a set
52 Iterates the metrics and selects the ones with the lowest dilution.
53 All the returned metrics are guaranteed to have the same dilution.
55 Args:
56 metrics: The metrics to pick the best ones from.
58 Returns: A list with the best metrics.
59 """
61 champs = []
63 # Find the best metric
64 for metric in metrics:
65 # Store the current value for easy access
66 dilution = metric.dilution
68 # First element, set the minimum to the current
69 if not champs:
70 min = dilution
72 # Worse than our best, not interesting
73 if dilution > min:
74 continue
76 # Clear the old ones if we have a better minimum
77 if dilution < min:
78 champs = []
79 min = dilution
81 champs.append(metric)
83 return min, champs
85 def _calculateDilution(target, start, parent_lists, global_memory, ignore=[]):
86 """Calculates the dilution for the specified commit.
88 Args:
89 target: The commit to calculate the dilution for.
90 start: The initial metric information.
91 parent_list: A dictionary with commits and their parents.
92 global_memory: A dictionary with all commits and their parents.
93 ignore: The parent commits of the target commit which should be ignored.
94 """
96 metrics = []
98 memory = {}
99 stack = []
100 stack.append(start)
102 git = Repo(".").git
104 while stack:
105 current = stack.pop()
107 # We found what we are looking for
108 if target == current.commit:
109 metrics.append(current)
110 continue
112 # Ignore commits that are past the target
113 if current.commit in ignore:
114 continue
116 # If we already checked this commit, don't add it again
117 if memory.has_key(current.commit):
118 dilution = memory[current.commit]
119 if dilution <= current.dilution:
120 continue
122 if global_memory.has_key(current.commit):
123 dilution = global_memory[current.commit]
124 if dilution < current.dilution:
125 continue
127 memory[current.commit] = current.dilution
128 global_memory[current.commit] = current.dilution
130 parents = parent_lists[current.commit]
132 # Root commit, we can't go any further
133 if not parents:
134 continue
136 for parent in parents:
137 if parent == parents[0]:
138 newdilution = current.dilution
139 else:
140 newdilution = current.dilution + 1
142 next = Metric()
143 next.commit = parent
144 next.branch = current.branch
145 next.dilution = newdilution
146 stack.append(next)
148 if not metrics:
149 return []
151 min, champs = _bestMetric(metrics)
152 return champs
154 def debugPrint(debug, text):
155 """Only print the specified text if debug is on
157 debug: Whether debug is on.
158 text: The text to print
161 if debug:
162 print(text)
164 def getBranchMap(branches, cut_off_prefix=True):
168 git = Repo(".").git
170 branch_map = {}
172 for branch in branches:
173 if cut_off_prefix:
174 branch = branch[2:]
176 branch_hash = git.rev_parse(branch)
178 if branch_hash in branch_map.values():
179 continue
181 branch_map[branch] = branch_hash
183 return branch_map
185 def getParentList(commits):
189 git = Repo(".").git
191 # First mine some data
192 result = git.rev_list("--parents", *commits)
193 data = result.split('\n')
195 parent_list = {}
197 for item in data:
198 splitline = item.split(' ')
199 first = splitline[0]
201 if len(splitline) == 1:
202 parent_list[first] = []
203 elif splitline[1] == '':
204 parent_list[first] = []
205 else:
206 parent_list[first] = splitline[1:]
208 return parent_list
210 def belongsTo(commit,
211 branch_map,
212 parent_list,
213 pretty_names={},
214 debug=False,
215 ignore_parents=False):
216 """Returns which branches the specified commit belongs to
218 Args:
219 commit: The commit to examine.
220 branch_map: A dictionary of all refs and their hashes.
221 parent_list: A dictionary with tuples of refs and their rev-lists.
222 pretty_names: A dictionary with commits and their names
223 debug: Whether to return debug information as well.
225 Returns: A list of branches that the commit belongs to.
228 git = Repo(".").git
230 if commit in branch_map:
231 debugPrint(debug, "Easy as pie, that's a branch head!")
232 name = branch_map[commit]
233 return [name]
235 metrics = []
237 if ignore_parents:
238 result = git.rev_list(commit)
239 ignore = set(result.split('\n'))
240 else:
241 ignore = []
243 debugPrint(debug, "Checking branches now:")
245 memory = {}
247 for branch_name, branch_hash in branch_map.iteritems():
248 debugPrint(debug, branch_name)
250 metric = Metric()
251 metric.branch = branch_hash
252 metric.commit = branch_hash
253 metric.dilution = 0
255 # Find the dilution for this branch
256 metric = _calculateDilution(commit, metric, parent_list, memory, ignore)
257 metrics = metrics + metric
259 debugPrint(debug, "Done.\n")
261 if debug:
262 print("Listing found metrics:")
263 for metric in metrics:
264 print(metric)
266 min, champs = _bestMetric(metrics)
268 debugPrint(debug, "Done.\n")
270 results = []
272 if debug:
273 results.append("The minimal dilution is: " + str(min))
275 # Loop over all the champs and get their name
276 for metric in champs:
277 results.append(prettyName(metric.branch, pretty_names))
279 return results
281 def branchcontains(branch, commit):
282 """returns whether the specified branch contains the specified commit.
284 params:
285 branch: the branch.
286 commit: the commit.
288 returns:
289 whether the branch contains the commit.
292 git = git(".")
293 arg = branch + ".." + commit
294 result = git.rev_list(arg)
296 if result:
297 # if there is a difference between these sets, the commit is not in the branch
298 return false
299 else:
300 # there is no difference between the two, thus the branch contains the commit
301 return true
303 def branchList(commitFilter, includeRemotes=False):
304 """Returns all branches that contain the specified commit
306 Args:
307 commitFilter: The commit to filter by.
308 includeRemotes: Whether to search remote branches as well.
311 git = Repo(".").git
313 args = []
315 if includeRemotes:
316 args.append("-a")
318 if commitFilter:
319 args.append("--contains")
320 args.append(commitFilter)
322 result = git.branch(*args)
324 branches = result.split('\n')
326 result = []
328 for branch in branches:
329 result.append(branch[2:])
331 return result
333 def dispatch(*args):
334 """Dispatches branch related commands
337 progname = os.path.basename(sys.argv[0]) + " branch"
339 parser = OptionParser(option_class=parse.GitOption, prog=progname)
342 parser.add_option(
343 "-b", "--belongs-to",
344 type="commit",
345 metavar="COMMIT",
346 help="find out which branch the specified commit belongs to")
348 parser.add_option(
349 "-d", "--debug",
350 action="store_true",
351 help="print debug information on the found metrics")
353 parser.add_option(
354 "-c", "--contains",
355 type="commit",
356 help="show only branches that contain the specified commit")
358 parser.add_option(
359 "-r", "--remotes",
360 action="store_true",
361 help="include remotes in the listing")
363 parser.set_default("debug", False)
364 parser.set_default("remotes", False)
366 (options, args) = parser.parse_args(list(args))
368 if options.belongs_to:
369 git = Repo(".").git
371 result = git.branch("-a", "--contains", options.belongs_to)
372 branches = result.split('\n')
374 branch_map = getBranchMap(branches)
375 parent_list = getParentList(branch_map.values())
377 result = belongsTo( options.belongs_to,
378 branch_map,
379 parent_list,
380 debug=options.debug)
381 else:
382 result = branchList(commitFilter=options.contains,
383 includeRemotes=options.remotes)
385 print("Matching branches:")
387 for branch in result:
388 print(branch)