gitstats: Two more memories were added and some refactoring
[git-stats.git] / src / git_stats / branch.py
blob4b0b6a1e183537da3461780168ad7044d0f7d000
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):
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 """
95 metrics = []
97 memory = {}
98 stack = []
99 stack.append(start)
101 git = Repo(".").git
103 while stack:
104 current = stack.pop()
106 # If we already checked this commit, don't add it again
107 if memory.has_key(current.commit):
108 dilution = memory[current.commit]
109 if dilution <= current.dilution:
110 continue
112 if global_memory.has_key(current.commit):
113 dilution = global_memory[current.commit]
114 if dilution < current.dilution:
115 continue
117 memory[current.commit] = current.dilution
118 global_memory[current.commit] = current.dilution
120 # We found what we are looking for
121 if target == current.commit:
122 metrics.append(current)
123 continue
125 parents = parent_lists[current.commit]
127 # Root commit, we can't go any further
128 if not parents:
129 continue
131 for parent in parents:
132 if parent == parents[0]:
133 newdilution = current.dilution
134 else:
135 newdilution = current.dilution + 1
137 next = Metric()
138 next.commit = parent
139 next.branch = current.branch
140 next.dilution = newdilution
141 stack.append(next)
143 if not metrics:
144 return []
146 min, champs = _bestMetric(metrics)
147 return champs
149 def debugPrint(debug, text):
150 """Only print the specified text if debug is on
152 debug: Whether debug is on.
153 text: The text to print
156 if debug:
157 print(text)
159 def getBranchMap(branches, cut_off_prefix=True):
163 git = Repo(".").git
165 branch_map = {}
167 for branch in branches:
168 if cut_off_prefix:
169 branch = branch[2:]
171 branch_hash = git.rev_parse(branch)
173 if branch_hash in branch_map.values():
174 continue
176 branch_map[branch] = branch_hash
178 return branch_map
180 def getParentList(commits):
184 git = Repo(".").git
186 # First mine some data
187 result = git.rev_list("--parents", *commits)
188 data = result.split('\n')
190 parent_list = {}
192 for item in data:
193 splitline = item.split(' ')
194 first = splitline[0]
196 if len(splitline) == 1:
197 parent_list[first] = []
198 elif splitline[1] == '':
199 parent_list[first] = []
200 else:
201 parent_list[first] = splitline[1:]
203 return parent_list
205 def belongsTo(commit, branch_map, parent_list, pretty_names={}, debug=False):
206 """Returns which branches the specified commit belongs to
208 Args:
209 commit: The commit to examine.
210 branch_map: A dictionary of all refs and their hashes.
211 parent_list: A dictionary with tuples of refs and their rev-lists.
212 debug: Whether to return debug information as well.
214 Returns: A list of branches that the commit belongs to.
217 git = Repo(".").git
219 if commit in branch_map:
220 debugPrint(debug, "Easy as pie, that's a branch head!")
221 name = branch_map[commit]
222 return [name]
224 metrics = []
226 debugPrint(debug, "Checking branches now:")
228 memory = {}
230 for branch_name, branch_hash in branch_map.iteritems():
231 debugPrint(debug, branch_name)
233 metric = Metric()
234 metric.branch = branch_hash
235 metric.commit = branch_hash
236 metric.dilution = 0
238 # Find the dilution for this branch
239 metric = _calculateDilution(commit, metric, parent_list, memory)
240 metrics = metrics + metric
242 debugPrint(debug, "Done.\n")
244 if debug:
245 print("Listing found metrics:")
246 for metric in metrics:
247 print(metric)
249 min, champs = _bestMetric(metrics)
251 debugPrint(debug, "Done.\n")
253 results = []
255 if debug:
256 results.append("The minimal dilution is: " + str(min))
258 # Loop over all the champs and get their name
259 for metric in champs:
260 results.append(prettyName(metric.branch, pretty_names))
262 return results
264 def branchcontains(branch, commit):
265 """returns whether the specified branch contains the specified commit.
267 params:
268 branch: the branch.
269 commit: the commit.
271 returns:
272 whether the branch contains the commit.
275 git = git(".")
276 arg = branch + ".." + commit
277 result = git.rev_list(arg)
279 if result:
280 # if there is a difference between these sets, the commit is not in the branch
281 return false
282 else:
283 # there is no difference between the two, thus the branch contains the commit
284 return true
286 def branchList(commitFilter, includeRemotes=False):
287 """Returns all branches that contain the specified commit
289 Args:
290 commitFilter: The commit to filter by.
291 includeRemotes: Whether to search remote branches as well.
294 git = Repo(".").git
296 args = []
298 if includeRemotes:
299 args.append("-a")
301 if commitFilter:
302 args.append("--contains")
303 args.append(commitFilter)
305 result = git.branch(*args)
307 branches = result.split('\n')
309 result = []
311 for branch in branches:
312 result.append(branch[2:])
314 return result
316 def dispatch(*args):
317 """Dispatches branch related commands
320 progname = os.path.basename(sys.argv[0]) + " branch"
322 parser = OptionParser(option_class=parse.GitOption, prog=progname)
325 parser.add_option(
326 "-b", "--belongs-to",
327 type="commit",
328 metavar="COMMIT",
329 help="find out which branch the specified commit belongs to")
331 parser.add_option(
332 "-d", "--debug",
333 action="store_true",
334 help="print debug information on the found metrics")
336 parser.add_option(
337 "-c", "--contains",
338 type="commit",
339 help="show only branches that contain the specified commit")
341 parser.add_option(
342 "-r", "--remotes",
343 action="store_true",
344 help="include remotes in the listing")
346 parser.set_default("debug", False)
347 parser.set_default("remotes", False)
349 (options, args) = parser.parse_args(list(args))
351 if options.belongs_to:
352 git = Repo(".").git
354 result = git.branch("-a", "--contains", options.belongs_to)
355 branches = result.split('\n')
357 branch_map = getBranchMap(branches)
358 parent_list = getParentList(branch_map.values())
360 result = belongsTo( options.belongs_to,
361 branch_map,
362 parent_list,
363 debug=options.debug)
364 else:
365 result = branchList(commitFilter=options.contains,
366 includeRemotes=options.remotes)
368 print("Matching branches:")
370 for branch in result:
371 print(branch)