gitstats: Disable debug output, print minimum dilution
[git-stats.git] / src / git_stats / branch.py
blob503b57ce829002e981cac17bbe1cb3344a7ef368
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 class Metric:
12 """This class represents a metric
14 It is meant to store arbitrary data related to a specific metric.
15 """
17 def __init__(self):
18 self.branch = ""
19 self.commit = ""
20 self.dilution = ""
22 def __str__(self):
23 res = "We found that on branch %s, commit %s has a dilution of %d." % \
24 (self.branch, self.commit, self.dilution)
26 return res
28 def prettyName(branch):
29 """Looks up the name of a branch and returns it
31 Returns only the last part of the branchname.
33 Args:
34 branch: The branch to print
35 """
37 git = Repo(".").git
39 name = git.describe("--all", branch)
40 splitname = name.split('/')
41 name = splitname[-1]
43 return name
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):
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 stack = []
93 stack.append(start)
95 git = Repo(".").git
97 while stack:
98 current = stack.pop()
100 # We found what we are looking for
101 if target == current.commit:
102 metrics.append(current)
103 continue
105 parents = parent_lists[current.commit]
107 # Root commit, we can't go any further
108 if not parents:
109 continue
111 for parent in parents:
112 if parent == parents[0]:
113 newdilution = current.dilution
114 else:
115 newdilution = current.dilution + 1
117 next = Metric()
118 next.commit = parent
119 next.branch = current.branch
120 next.dilution = newdilution
121 stack.append(next)
123 min, champs = _bestMetric(metrics)
124 return champs
126 def belongsTo(commit, debug=False):
127 """Returns which branches the specified commit belongs to
129 Args:
130 commit: The commit to examine.
131 debug: Whether to return debug information as well.
133 Returns: A list of branches that the commit belongs to.
136 git = Repo(".").git
138 result = git.for_each_ref(format="%(objectname)")
140 branches = result.split('\n')
142 if commit in branches:
143 print("Easy as pie, that's a branch head!")
144 return [prettyName(commit)]
146 metrics = []
147 rev_lists = {}
148 parent_lists = {}
149 check = []
151 # First mine some data
152 for branch in branches:
153 result = git.log(branch, pretty="format:%H %P")
154 data = result.split('\n')
156 parents = []
158 for item in data:
159 splitline = item.split(' ')
160 first = splitline[0]
161 parents.append(first)
163 if len(splitline) == 1:
164 parent_lists[first] = []
165 elif splitline[1] == '':
166 parent_lists[first] = []
167 else:
168 parent_lists[first] = splitline[1:]
170 rev_lists[branch] = parents
172 # Early elimination, examine only those that contain the target
173 if commit in parents:
174 check.append(branch)
176 for branch in check:
177 metric = Metric()
178 metric.branch = branch
179 metric.commit = branch
180 metric.dilution = 0
182 # Find the dilution for this branch
183 metric = _calculateDilution(commit, metric, parent_lists)
184 metrics = metrics + metric
186 min, champs = _bestMetric(metrics)
188 results = []
189 results.append("The minimal dilution is: " + str(min))
191 # Loop over all the champs and exclude any subsets
192 for metric in champs:
193 champlist = rev_lists[metric.branch]
195 for othermetric in champs:
196 # Don't compare to self
197 if metric.branch == othermetric.branch:
198 continue
200 otherlist = rev_lists[othermetric.branch]
202 if set(champlist).issubset(set(otherlist)):
203 break
204 else:
205 if debug:
206 results.append(metric)
207 else:
208 results.append(prettyName(metric.branch))
210 return results
212 def branchcontains(branch, commit):
213 """returns whether the specified branch contains the specified commit.
215 params:
216 branch: the branch.
217 commit: the commit.
219 returns:
220 whether the branch contains the commit.
223 git = git(".")
224 arg = branch + ".." + commit
225 result = git.rev_list(arg)
227 if result:
228 # if there is a difference between these sets, the commit is not in the branch
229 return false
230 else:
231 # there is no difference between the two, thus the branch contains the commit
232 return true
234 def branchList(commitFilter, includeRemotes=False):
235 """Returns all branches that contain the specified commit
237 Args:
238 commitFilter: The commit to filter by.
239 includeRemotes: Whether to search remote branches as well.
242 git = Repo(".").git
244 args = []
246 if includeRemotes:
247 args.append("-a")
249 if commitFilter:
250 args.append("--contains")
251 args.append(commitFilter)
253 result = git.branch(*args)
255 branches = result.split('\n')
257 result = []
259 for branch in branches:
260 result.append(branch[2:])
262 return result
264 def dispatch(*args):
265 """Dispatches branch related commands
268 progname = os.path.basename(sys.argv[0]) + " branch"
270 parser = OptionParser(option_class=parse.GitOption, prog=progname)
273 parser.add_option(
274 "-b", "--belongs-to",
275 type="commit",
276 metavar="COMMIT",
277 help="find out which branch the specified commit belongs to")
279 parser.add_option(
280 "-d", "--debug",
281 action="store_true",
282 help="print debug information on the found metrics")
284 parser.add_option(
285 "-c", "--contains",
286 type="commit",
287 help="show only branches that contain the specified commit")
289 parser.add_option(
290 "-r", "--remotes",
291 action="store_true",
292 help="include remotes in the listing")
294 parser.set_default("debug", False)
295 parser.set_default("remotes", False)
297 (options, args) = parser.parse_args(list(args))
299 if options.belongs_to:
300 result = belongsTo(options.belongs_to, options.debug)
301 else:
302 result = branchList(commitFilter=options.contains,
303 includeRemotes=options.remotes)
305 print("Matching branches:")
307 for branch in result:
308 print(branch)