Refactored bug.py to use a Memory and Options class
[git-stats.git] / src / git_stats / bug.py
blob3060983d846d235ab47287fc3068a538c1642c4c
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 class CommitInfo:
16 """Contains information about a specific commit
17 """
19 def __init__(self, commit):
20 """Initializes with the specified commit
21 """
23 self.commit = commit
24 self.branches = []
25 self.reverts = []
26 self.msg_matches = ""
27 self.diff_matches = ""
29 def isBugfix():
30 """Checks if this commit is a bugfix
32 Returns: Whether branches, reverts, msg_matches or diff_matches is set.
33 """
35 return self.branches \
36 or self.reverts \
37 or self.msg_matches \
38 or self.diff_matches
40 def __str__(self):
41 """Returns a string representation of this object
42 """
44 result = []
46 for branch in self.branches:
47 result.append("In branch: " + branch)
49 for commit in self.reverts:
50 result.append("Reverts commit: " + commit)
52 if self.msg_matches:
53 result.append("Message matches: '" + self.msg_matches + "'.")
55 if self.diff_matches:
56 result.append("Diff matches: '" + self.diff_matches + "'.")
58 res = self.commit + ":"
60 for line in result:
61 res += "\n" + line
63 return res
65 class Memory():
66 """Storage class.
68 Fields:
69 branch_map: A dictionary of all refs and their hashes.
70 parent_list: A dictionary with tuples of refs and their rev-lists.
71 raw_diffs: A dictionary with commits and their raw diffs.
72 parsed_diffs: A dictionary with commits and their parsed diffs.
73 touched_files: A dictionary with files and the commits that touched them.
74 pretty_names: A dictionary with commits and their names
75 """
77 def __init__(self):
78 """Initializes the memory to an empty state
79 """
81 git = Repo(".").git
83 self.pretty_names = {}
84 self.raw_diffs = {}
85 self.parsed_diffs = {}
86 self.touched_files = {}
88 result = git.for_each_ref("refs/heads",
89 "refs/remotes",
90 format="%(refname)")
92 branches = result.split('\n')
93 self.branch_map = branch.getBranchMap(branches, cut_off_prefix=False)
94 self.parent_list = branch.getParentList(self.branch_map.values())
96 for name, hash in self.branch_map.iteritems():
97 # Skip the leading 'refs/[heads|remotes]' prefix
98 pos = name.find('/', 5)
99 pos += 1
100 name = name[pos:]
101 self.pretty_names[hash] = name
103 class Options():
104 """Options class
107 def __init__(self):
108 """Creates a new Options instance.
111 pass
113 def aggregateType(options):
114 """Uses the specified options to get type information for many commits
117 git = Repo(".").git
118 result = git.rev_list(options.start_from)
119 commits = result.split('\n')
121 options = extractOptions(options)
123 # Unless overridden by the user ignore parents for large repositories
124 if len(commits) > 1000 and options.ignore_parents == None:
125 options.ignore_parents = True
127 memory = Memory()
129 result = []
131 if options.limit:
132 commits = commits[:options.limit]
134 for commit in commits:
135 if options.debug:
136 print(commit)
138 stats = determineType(commit, memory, options)
139 result.append(stats)
141 return result
143 def determineType(target, memory, options):
144 """Determines the type of the specified commit
146 Args:
147 commit: The commit to determine the type of.
148 memory: A class containing various storage containers.
149 options: A dictionary containing options.
152 branches = branch.belongsTo(target,
153 memory.branch_map,
154 memory.parent_list,
155 pretty_names=memory.pretty_names,
156 ignore_parents=options.ignore_parents)
158 reverts = diff.findReverts( target,
159 raw_diffs=memory.raw_diffs,
160 parsed_diffs=memory.parsed_diffs,
161 touched_files=memory.touched_files)
163 msg_matches = options.msg_filter and \
164 commit.commitmsgMatches(target, options.msg_filter)
166 diff_matches = options.diff_filter and \
167 commit.commitdiffMatches( target,
168 options.diff_filter,
169 raw_diffs=memory.raw_diffs)
171 result = CommitInfo(target)
173 filter_branches = options.branch_filter.split(':')
174 match = set(filter_branches).intersection(set(branches))
176 if match:
177 result.branches = match
179 if reverts:
180 result.reverts = reverts
182 if msg_matches:
183 result.msg_matches = options.msg_filter
185 if diff_matches:
186 result.diff_matches = options.diff_filter
188 return result
190 def extractOptions(options):
191 """Extracts options and returns the result
193 The options are extracted from the specified options and
194 from the 'config' file.
197 opts = config.read()
199 result = Options()
201 options_list = ["branch_filter",
202 "debug",
203 "diff_filter",
204 "msg_filter",
205 "ignore_parents",
206 "limit"]
208 for opt in options_list:
209 if getattr(options, opt, None) == None:
210 val = opts.get(opt, None)
211 else:
212 val = getattr(options, opt)
214 setattr(result, opt, val)
216 return result
218 def _checkOptions(parser, options):
219 """Checks the specified options and uses the parser to indicate errors
221 Args:
222 parser: The parser to use to signal when the options are bad.
223 options: The options to check.
226 opts = [options.aggregate, options.type]
228 if not parse.isUnique(opts):
229 parser.error("Please choose exactly one mode")
231 def dispatch(*args):
232 """Dispatches index related commands
235 progname = os.path.basename(sys.argv[0]) + " bug"
237 parser = OptionParser(option_class=parse.GitOption, prog=progname)
239 parser.add_option(
240 "-d", "--debug",
241 action="store_true",
242 help="show debug information")
244 parser.add_option(
245 "-a", "--aggregate",
246 action="store_true",
247 help="aggregate bug information of all commits")
249 parser.add_option(
250 "-t", "--type",
251 type="commit",
252 help="shows which type the specified commit is")
254 parser.add_option(
255 "-s", "--start-from",
256 type="commit",
257 metavar="COMMIT",
258 help="the commit to start from")
260 parser.add_option(
261 "-m", "--message-filter",
262 help="mark the commit as a fix if it's log matches this filter")
264 parser.add_option(
265 "-f", "--diff-filter",
266 help="mark the commit as a fix if it's diff matches this filter")
268 parser.add_option(
269 "-b", "--branch-filter",
270 help="mark the commit as a fix if it belongs to this branch")
272 parser.add_option(
273 "-l", "--limit",
274 type="int",
275 metavar="N",
276 help="limit the commits checked to the most recent N ones")
278 parser.add_option(
279 "-i", "--ignore-parents",
280 type="bool",
281 help="whether to enable parentage ignoring")
283 # Default to True for now, until there is another option
284 parser.set_default("debug", False)
285 parser.set_default("aggregate", False)
286 parser.set_default("start_from", "HEAD")
287 parser.set_default("limit", 10)
288 parser.set_default("ignore_parents", None)
290 (options, args) = parser.parse_args(list(args))
292 _checkOptions(parser, options)
294 if options.aggregate:
295 result = aggregateType(options)
296 elif options.type:
297 kwargs = extractOptions(options)
298 memory = Memory()
299 stats = determineType(options.type, memory, kwargs)
300 result = [str(stats)]
302 for line in result:
303 print(line)