gitstats: Add a 'commits that touch' memory to commit.py
[git-stats.git] / src / git_stats / branch.py
blobfd0f082f846c1a13523cc04faaa77a2ba55229c2
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):
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 """
20 git = Repo(".").git
22 name = git.describe("--all", branch)
23 splitname = name.split('/')
24 name = splitname[-1]
26 return name
28 class Metric:
29 """This class represents a metric
31 It is meant to store arbitrary data related to a specific metric.
32 """
34 def __init__(self):
35 self.branch = ""
36 self.commit = ""
37 self.dilution = ""
39 def __str__(self):
40 res = "Branch %s, dilution: %d." % \
41 (prettyName(self.branch), self.dilution)
43 return res
45 def _bestMetric(metrics):
46 """Finds the best metric from a set
48 Iterates the metrics and selects the ones with the lowest dilution.
49 All the returned metrics are guaranteed to have the same dilution.
51 Args:
52 metrics: The metrics to pick the best ones from.
54 Returns: A list with the best metrics.
55 """
57 champs = []
59 # Find the best metric
60 for metric in metrics:
61 # Store the current value for easy access
62 dilution = metric.dilution
64 # First element, set the minimum to the current
65 if not champs:
66 min = dilution
68 # Worse than our best, not interesting
69 if dilution > min:
70 continue
72 # Clear the old ones if we have a better minimum
73 if dilution < min:
74 champs = []
75 min = dilution
77 champs.append(metric)
79 return min, champs
81 def _calculateDilution(target, start, parent_lists, global_memory):
82 """Calculates the dilution for the specified commit.
84 Args:
85 target: The commit to calculate the dilution for.
86 start: The initial metric information.
87 parent_list: A dictionary containing { commit : parent } pairs.
88 """
90 metrics = []
92 memory = {}
93 stack = []
94 stack.append(start)
96 git = Repo(".").git
98 while stack:
99 current = stack.pop()
101 # If we already checked this commit, don't add it again
102 if memory.has_key(current.commit):
103 dilution = memory[current.commit]
104 if dilution <= current.dilution:
105 continue
107 if global_memory.has_key(current.commit):
108 dilution = global_memory[current.commit]
109 if dilution < current.dilution:
110 continue
112 memory[current.commit] = current.dilution
113 global_memory[current.commit] = current.dilution
115 # We found what we are looking for
116 if target == current.commit:
117 metrics.append(current)
118 continue
120 parents = parent_lists[current.commit]
122 # Root commit, we can't go any further
123 if not parents:
124 continue
126 for parent in parents:
127 if parent == parents[0]:
128 newdilution = current.dilution
129 else:
130 newdilution = current.dilution + 1
132 next = Metric()
133 next.commit = parent
134 next.branch = current.branch
135 next.dilution = newdilution
136 stack.append(next)
138 if not metrics:
139 return []
141 min, champs = _bestMetric(metrics)
142 return champs
144 def debugPrint(debug, text):
145 """Only print the specified text if debug is on
147 debug: Whether debug is on.
148 text: The text to print
151 if debug:
152 print(text)
154 def belongsTo(commit, debug=False):
155 """Returns which branches the specified commit belongs to
157 Args:
158 commit: The commit to examine.
159 debug: Whether to return debug information as well.
161 Returns: A list of branches that the commit belongs to.
164 git = Repo(".").git
166 result = git.for_each_ref("refs/heads", "refs/remotes", format="%(objectname)")
168 branches = result.split('\n')
170 if commit in branches:
171 debugPrint(debug, "Easy as pie, that's a branch head!")
172 return [prettyName(commit)]
174 metrics = []
175 rev_lists = {}
176 parent_lists = {}
177 check = []
179 debugPrint(debug, "Retreiving branches that contain the commit...")
181 result = git.branch("-a", "--contains", commit)
182 branches = result.split('\n')
184 branch_map = {}
186 for branch in branches:
187 branch_name = branch[2:]
188 branch_hash = git.rev_parse(branch_name)
190 if branch_hash in branch_map.values():
191 continue
193 branch_map[branch_name] = branch_hash
195 debugPrint(debug, "Gathering information on the history...")
197 # First mine some data
198 args = branch_map.values()
199 result = git.rev_list("--parents", *args)
200 data = result.split('\n')
202 for item in data:
203 splitline = item.split(' ')
204 first = splitline[0]
206 if len(splitline) == 1:
207 parent_lists[first] = []
208 elif splitline[1] == '':
209 parent_lists[first] = []
210 else:
211 parent_lists[first] = splitline[1:]
213 debugPrint(debug, "Done.\n")
215 debugPrint(debug, "Checking branches now:")
217 memory = {}
219 for branch_name, branch_hash in branch_map.iteritems():
220 debugPrint(debug, branch_name)
222 metric = Metric()
223 metric.branch = branch_hash
224 metric.commit = branch_hash
225 metric.dilution = 0
227 # Find the dilution for this branch
228 metric = _calculateDilution(commit, metric, parent_lists, memory)
229 metrics = metrics + metric
231 debugPrint(debug, "Done.\n")
233 if debug:
234 print("Listing found metrics:")
235 for metric in metrics:
236 print(metric)
238 min, champs = _bestMetric(metrics)
240 debugPrint(debug, "Done.\n")
242 results = []
244 if debug:
245 results.append("The minimal dilution is: " + str(min))
247 # Loop over all the champs and get their name
248 for metric in champs:
249 results.append(prettyName(metric.branch))
251 return results
253 def branchcontains(branch, commit):
254 """returns whether the specified branch contains the specified commit.
256 params:
257 branch: the branch.
258 commit: the commit.
260 returns:
261 whether the branch contains the commit.
264 git = git(".")
265 arg = branch + ".." + commit
266 result = git.rev_list(arg)
268 if result:
269 # if there is a difference between these sets, the commit is not in the branch
270 return false
271 else:
272 # there is no difference between the two, thus the branch contains the commit
273 return true
275 def branchList(commitFilter, includeRemotes=False):
276 """Returns all branches that contain the specified commit
278 Args:
279 commitFilter: The commit to filter by.
280 includeRemotes: Whether to search remote branches as well.
283 git = Repo(".").git
285 args = []
287 if includeRemotes:
288 args.append("-a")
290 if commitFilter:
291 args.append("--contains")
292 args.append(commitFilter)
294 result = git.branch(*args)
296 branches = result.split('\n')
298 result = []
300 for branch in branches:
301 result.append(branch[2:])
303 return result
305 def dispatch(*args):
306 """Dispatches branch related commands
309 progname = os.path.basename(sys.argv[0]) + " branch"
311 parser = OptionParser(option_class=parse.GitOption, prog=progname)
314 parser.add_option(
315 "-b", "--belongs-to",
316 type="commit",
317 metavar="COMMIT",
318 help="find out which branch the specified commit belongs to")
320 parser.add_option(
321 "-d", "--debug",
322 action="store_true",
323 help="print debug information on the found metrics")
325 parser.add_option(
326 "-c", "--contains",
327 type="commit",
328 help="show only branches that contain the specified commit")
330 parser.add_option(
331 "-r", "--remotes",
332 action="store_true",
333 help="include remotes in the listing")
335 parser.set_default("debug", False)
336 parser.set_default("remotes", False)
338 (options, args) = parser.parse_args(list(args))
340 if options.belongs_to:
341 result = belongsTo(options.belongs_to, options.debug)
342 else:
343 result = branchList(commitFilter=options.contains,
344 includeRemotes=options.remotes)
346 print("Matching branches:")
348 for branch in result:
349 print(branch)