gitstats: Use format specifiers instead of appending to a string
[git-stats.git] / src / git_stats / bug.py
blob15e4bcd5bd74654b8dc5ac87cd98b62a7f5a7f7d
1 #!/usr/bin/env python
3 import os
4 import sys
6 from optparse import OptionParser, OptionValueError
7 from git import Repo
9 from git_stats import branch
10 from git_stats import commit
11 from git_stats import config
12 from git_stats import diff
13 from git_stats import parse
15 options_list = [
16 "branch_filter",
17 "branch_filter_rating",
18 "debug",
19 "diff_filter",
20 "diff_filter_rating",
21 "msg_filter",
22 "msg_filter_rating",
23 "ignore_parents",
24 "limit",
25 "revert_rating",
28 class CommitInfo:
29 """Contains information about a specific commit
30 """
32 def __init__(self, commit, options):
33 """Initializes with the specified commit
34 """
36 self.options = options
37 self.commit = commit
38 self.branches = []
39 self.reverts = []
40 self.msg_matches = ""
41 self.diff_matches = ""
43 def isBugfix(self):
44 """Checks if this commit is a bugfix
46 Returns: Whether branches, reverts, msg_matches or diff_matches is set.
47 """
49 return self.branches \
50 or self.reverts \
51 or self.msg_matches \
52 or self.diff_matches
54 def bugfixRating(self):
55 """Calculates the bugfix rating for this commit
56 """
58 score = 0
60 if self.branches and getattr(self.options, "branch_filter_rating", None):
61 score += self.options.branch_filter_rating
63 if self.reverts and getattr(self.options, "reverts_rating", None):
64 score += self.options.reverts_rating
66 if self.msg_matches and getattr(self.options, "msg_filter_rating", None):
67 score += self.options.msg_filter_rating
69 if self.diff_matches and getattr(self.options, "diff_filter_rating", None):
70 score += self.options.diff_filter_rating
72 return score
74 def __str__(self):
75 """Returns a string representation of this object
76 """
78 result = []
80 for branch in self.branches:
81 result.append("In branch: '%s'." % branch)
83 for commit in self.reverts:
84 result.append("Reverts commit: '%s'." % commit)
86 if self.msg_matches:
87 result.append("Message matches: '%s'." % self.msg_matches)
89 if self.diff_matches:
90 result.append("Diff matches: '%s'." % self.diff_matches)
92 if self.isBugfix():
93 result.append("Bugfix rating: %d." % self.bugfixRating())
95 res = self.commit + ":"
97 for line in result:
98 res = "%s\n%s" % (res,line)
100 return res
102 class Memory():
103 """Storage class.
105 Fields:
106 branch_map: A dictionary of all refs and their hashes.
107 parent_list: A dictionary with tuples of refs and their rev-lists.
108 raw_diffs: A dictionary with commits and their raw diffs.
109 parsed_diffs: A dictionary with commits and their parsed diffs.
110 touched_files: A dictionary with files and the commits that touched them.
111 pretty_names: A dictionary with commits and their names
114 def __init__(self):
115 """Initializes the memory to an empty state
118 git = Repo(".").git
120 self.pretty_names = {}
121 self.raw_diffs = {}
122 self.parsed_diffs = {}
123 self.touched_files = {}
125 result = git.for_each_ref("refs/heads",
126 "refs/remotes",
127 format="%(refname)")
129 branches = result.split('\n')
130 self.branch_map = branch.getBranchMap(branches)
131 self.parent_list = branch.getParentList(self.branch_map.values())
133 for name, hash in self.branch_map.iteritems():
134 # Skip the leading 'refs/[heads|remotes]' prefix
135 pos = name.find('/', 5)
136 pos += 1
137 name = name[pos:]
138 self.pretty_names[hash] = name
140 def aggregateType(options):
141 """Uses the specified options to get type information for many commits
144 git = Repo(".").git
145 result = git.rev_list(options.start_from)
146 commits = result.split('\n')
148 options = config.extractOptions(options, options_list)
150 # Unless overridden by the user ignore parents for large repositories
151 if len(commits) > 1000 and options.ignore_parents == None:
152 options.ignore_parents = True
154 memory = Memory()
156 result = []
158 if options.limit:
159 commits = commits[:options.limit]
161 for commit in commits:
162 if options.debug:
163 print(commit)
165 stats = determineType(commit, memory, options)
166 result.append(stats)
168 return result
170 def determineType(target, memory, options):
171 """Determines the type of the specified commit
173 Args:
174 commit: The commit to determine the type of.
175 memory: A class containing various storage containers.
176 options: A dictionary containing options.
179 branches = branch.belongsTo(target,
180 memory.branch_map,
181 memory.parent_list,
182 pretty_names=memory.pretty_names,
183 ignore_parents=options.ignore_parents)
185 reverts = diff.findReverts( target,
186 raw_diffs=memory.raw_diffs,
187 parsed_diffs=memory.parsed_diffs,
188 touched_files=memory.touched_files)
190 msg_matches = options.msg_filter and \
191 commit.commitmsgMatches(target, options.msg_filter)
193 diff_matches = options.diff_filter and \
194 commit.commitdiffMatches( target,
195 options.diff_filter,
196 raw_diffs=memory.raw_diffs)
198 result = CommitInfo(target, options)
200 if not options.branch_filter:
201 match = None
202 else:
203 filter_branches = options.branch_filter.split(':')
204 match = set(filter_branches).intersection(set(branches))
206 if match:
207 result.branches = match
209 if reverts:
210 result.reverts = reverts
212 if msg_matches:
213 result.msg_matches = options.msg_filter
215 if diff_matches:
216 result.diff_matches = options.diff_filter
218 return result
220 def _checkOptions(parser, options):
221 """Checks the specified options and uses the parser to indicate errors
223 Args:
224 parser: The parser to use to signal when the options are bad.
225 options: The options to check.
228 opts = [options.aggregate, options.type]
230 if not parse.isUnique(opts, atLeastOne=True):
231 parser.error("Please choose exactly one mode")
233 def dispatch(*args):
234 """Dispatches index related commands
237 progname = os.path.basename(sys.argv[0]) + " bug"
239 parser = OptionParser(option_class=parse.GitOption, prog=progname)
241 parser.add_option(
242 "-d", "--debug",
243 action="store_true",
244 help="show debug information")
246 parser.add_option(
247 "-a", "--aggregate",
248 action="store_true",
249 help="aggregate bug information of all commits")
251 parser.add_option(
252 "-t", "--type",
253 type="commit",
254 help="shows which type the specified commit is")
256 parser.add_option(
257 "-s", "--start-from",
258 type="commit",
259 metavar="COMMIT",
260 help="the commit to start from")
262 parser.add_option(
263 "-m", "--message-filter",
264 help="mark the commit as a fix if it's log matches this filter")
266 parser.add_option(
267 "-f", "--diff-filter",
268 help="mark the commit as a fix if it's diff matches this filter")
270 parser.add_option(
271 "-b", "--branch-filter",
272 help="mark the commit as a fix if it belongs to this branch")
274 parser.add_option(
275 "-l", "--limit",
276 type="int",
277 metavar="N",
278 help="limit the commits checked to the most recent N ones")
280 parser.add_option(
281 "-i", "--ignore-parents",
282 type="bool",
283 help="whether to enable parentage ignoring")
285 # Default to True for now, until there is another option
286 parser.set_default("debug", False)
287 parser.set_default("aggregate", False)
288 parser.set_default("start_from", "HEAD")
289 parser.set_default("limit", 10)
290 parser.set_default("ignore_parents", None)
292 (options, args) = parser.parse_args(list(args))
294 _checkOptions(parser, options)
296 if options.aggregate:
297 result = aggregateType(options)
298 elif options.type:
299 kwargs = config.extractOptions(options, options_list)
300 memory = Memory()
301 stats = determineType(options.type, memory, kwargs)
302 result = [str(stats)]
304 for line in result:
305 print(line)