gitstats: Replace backslashes with parens to do line continuation
[git-stats.git] / src / git_stats / branch.py
blobd30d4495cb1e517b3fa916eb8af9d773e5360c9d
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 branch in pretty_names:
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 current.commit in memory:
118 dilution = memory[current.commit]
119 if dilution <= current.dilution:
120 continue
122 # Nor when already checked in a better branch
123 if current.commit in global_memory:
124 dilution = global_memory[current.commit]
125 if dilution < current.dilution:
126 continue
128 # Remember this commit
129 memory[current.commit] = current.dilution
130 global_memory[current.commit] = current.dilution
132 # Defaulting to None in the case we only have a partial parent listing
133 parents = parent_lists.get(current.commit, None)
135 # Root commit, we can't go any further
136 if not parents:
137 continue
139 # Check all the parents and give only the first one 0 extra dilution
140 for parent in parents:
141 if parent == parents[0]:
142 newdilution = current.dilution
143 else:
144 newdilution = current.dilution + 1
146 # Create the next target
147 next = Metric()
148 next.commit = parent
149 next.branch = current.branch
150 next.dilution = newdilution
152 # Add it to the stack to be examined
153 stack.append(next)
155 # Nothing new was found here
156 if not metrics:
157 return []
159 # Return only the best metric
160 min, champs = _bestMetric(metrics)
161 return champs
163 def debugPrint(debug, text):
164 """Only print the specified text if debug is on
166 debug: Whether debug is on.
167 text: The text to print
170 if debug:
171 print(text)
173 def getBranchMap(branches):
174 """Creates a branchMap from the specified branches and returns it
176 The hash of each branch is retreived and stored in a
177 dictionary with the condition that all values in the map
178 are unique. As such if multiple branches map to one hash,
179 only one of those branches (the first one) will be in the
180 returned map.
182 Args:
183 branches: A list of branches that should be in the map.
185 Returns: A dictionary with branch names and their hashes.
188 git = Repo(".").git
190 branch_map = {}
192 for branch in branches:
193 # Retreive the hash of each branch
194 branch_hash = git.rev_parse(branch)
196 if branch_hash in branch_map.values():
197 continue
199 branch_map[branch] = branch_hash
201 return branch_map
203 def getParentList(commits):
204 """Retreives the parents of the specified commits and their parents
206 The parents for all the specified commits, and for the
207 parents of those commits, are retreived and stored in
208 a dictionary.
210 Args:
211 commits: A list of commits for which the parents should be retreived.
213 Returns: A dictionary with commits and their parents.
216 git = Repo(".").git
218 # First mine some data
219 result = git.rev_list("--parents", *commits)
220 data = result.split('\n')
222 parent_list = {}
224 for item in data:
225 # Each line contains the commit first, and then all it's parents
226 splitline = item.split(' ')
227 first = splitline[0]
229 # This commit has no parents at all
230 if len(splitline) == 1:
231 parent_list[first] = []
232 # This commits has no parents either
233 elif splitline[1] == '':
234 parent_list[first] = []
235 # Store all the parents
236 else:
237 parent_list[first] = splitline[1:]
239 return parent_list
241 def belongsTo(commit,
242 branch_map,
243 parent_list,
244 pretty_names={},
245 debug=False,
246 ignore_parents=False):
247 """Returns which branches the specified commit belongs to
249 Args:
250 commit: The commit to examine.
251 branch_map: A dictionary of all refs and their hashes.
252 parent_list: A dictionary with tuples of refs and their rev-lists.
253 pretty_names: A dictionary with commits and their names
254 debug: Whether to return debug information as well.
256 Returns: A list of branches that the commit belongs to.
259 git = Repo(".").git
261 if commit in branch_map:
262 debugPrint(debug, "Easy as pie, that's a branch head!")
263 name = branch_map[commit]
264 return [name]
266 metrics = []
268 # Retreive the parents of the target commit and ignore them
269 if ignore_parents:
270 result = git.rev_list(commit)
271 ignore = set(result.split('\n'))
272 else:
273 ignore = []
275 debugPrint(debug, "Checking branches now:")
277 memory = {}
279 # Collect the metric for all branches
280 for branch_name, branch_hash in branch_map.iteritems():
281 debugPrint(debug, branch_name)
283 # Create the first metric
284 metric = Metric()
285 metric.branch = branch_hash
286 metric.commit = branch_hash
287 metric.dilution = 0
289 # Find the dilution for this branch
290 metric = _calculateDilution(commit, metric, parent_list, memory, ignore)
291 metrics = metrics + metric
293 debugPrint(debug, "Done.\n")
295 if debug:
296 print("Listing found metrics:")
297 for metric in metrics:
298 print(metric)
300 min, champs = _bestMetric(metrics)
302 debugPrint(debug, "Done.\n")
304 results = []
306 if debug:
307 results.append("The minimal dilution is: " + str(min))
309 # Loop over all the champs and get their name
310 for metric in champs:
311 results.append(prettyName(metric.branch, pretty_names))
313 return results
315 def branchcontains(branch, commit):
316 """returns whether the specified branch contains the specified commit.
318 params:
319 branch: the branch.
320 commit: the commit.
322 returns:
323 whether the branch contains the commit.
326 git = git(".")
327 arg = branch + ".." + commit
328 result = git.rev_list(arg)
330 if result:
331 # if there is a difference between these sets, the commit is not in the branch
332 return false
333 else:
334 # there is no difference between the two, thus the branch contains the commit
335 return true
337 def branchList(commitFilter, includeRemotes=False):
338 """Returns all branches that contain the specified commit
340 Args:
341 commitFilter: The commit to filter by.
342 includeRemotes: Whether to search remote branches as well.
345 git = Repo(".").git
347 args = []
349 if includeRemotes:
350 args.append("-a")
352 if commitFilter:
353 args.append("--contains")
354 args.append(commitFilter)
356 result = git.branch(*args)
358 branches = result.split('\n')
360 result = []
362 for branch in branches:
363 result.append(branch[2:])
365 return result
367 def dispatch(*args):
368 """Dispatches branch related commands
371 progname = os.path.basename(sys.argv[0]) + " branch"
373 parser = OptionParser(option_class=parse.GitOption, prog=progname)
376 parser.add_option(
377 "-b", "--belongs-to",
378 type="commit",
379 metavar="COMMIT",
380 help="find out which branch the specified commit belongs to")
382 parser.add_option(
383 "-d", "--debug",
384 action="store_true",
385 help="print debug information on the found metrics")
387 parser.add_option(
388 "-c", "--contains",
389 type="commit",
390 help="show only branches that contain the specified commit")
392 parser.add_option(
393 "-r", "--remotes",
394 action="store_true",
395 help="include remotes in the listing")
397 parser.set_default("debug", False)
398 parser.set_default("remotes", False)
400 (options, args) = parser.parse_args(list(args))
402 if options.belongs_to:
403 branches = branchList(options.belongs_to, includeRemotes=options.remotes)
405 branch_map = getBranchMap(branches)
406 parent_list = getParentList(branch_map.values())
408 result = belongsTo( options.belongs_to,
409 branch_map,
410 parent_list,
411 debug=options.debug)
412 else:
413 result = branchList(commitFilter=options.contains,
414 includeRemotes=options.remotes)
416 print("Matching branches:")
418 for branch in result:
419 print(branch)