gitstats: Added a "author -f" aggregation, and a test case.
[git-stats.git] / src / git_stats / author.py
blob568ef8b707bd62826bb180b48e242452f33b9d0f
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 parse
11 class Activity:
12 """Simple storage class containing stats on the activity in one file."""
14 def __init__(self):
15 self.count = 1
16 self.added = 0
17 self.deleted = 0
18 self.id = []
20 def __str__(self):
21 return str(self.count) + ": +" + str(self.added) + " -" + str(self.deleted)
23 def activityInArea(log, filterid):
24 """Parses the specified file containing commit logs.
25 The output is filtered to contain only the results of the specified author.
26 The output is expected to be in the format described below:
28 [<id>\n]+
29 [<lines added>\t<lines deleted>\t<path>]+
33 Params:
34 log: The log formatted as described above.
35 filterid: The id address to filter on.
37 Returns:
38 A dictionary containing activity per path is returned.
39 Each path contains one Activity object with the aggregated results.
40 """
42 # Create a dict to store the activity stats per path
43 activityByPath = {}
45 # Create a place to store the result in
46 activity = Activity()
48 i = 0
50 # Parse all the lines in the file
51 for line in log:
52 i += 1
54 # Split the line at the tab and store the data
55 splitline = line.split('\t')
56 size = len(splitline)
57 length = len(line.lstrip())
59 # There is something on this line, but it contains no separator
60 if size == 1 and length > 0:
61 # Get the id address minus the newline
62 activity.id.append(line[:-1])
63 elif size == 3:
64 try:
65 addpart = splitline[0]
66 deletepart = splitline[1]
68 if addpart == '-':
69 addpart = 0
71 if deletepart == '-':
72 deletepart = 0
74 activity.added = int(addpart)
75 activity.deleted = int(deletepart)
76 except ValueError, e:
77 print("On line '" + str(i) + "', could not convert number: " + str(e))
79 path = splitline[2][:-1]
80 elif length == 0:
81 if not filterid in activity.id:
82 # Ignore other authors
83 pass
84 else:
85 result = Activity()
87 # If we already encountered this path, update the count
88 if activityByPath.has_key(path):
89 known = activityByPath[path]
90 result.added = known.added + activity.added
91 result.deleted = known.deleted + activity.deleted
92 result.count = known.count + activity.count
93 else:
94 result = activity
96 # Store it under it's path
97 activityByPath[path] = result
99 # Create a fresh activity to store the next round in
100 activity = Activity()
101 else:
102 print("Cannot parse line " + str(i) + ".")
104 # Return the result
105 return activityByPath
107 def activityInFile(path, id, start_from, relative):
108 """Shows the activity for the file in the current repo.
110 Params:
111 path: The path to filter on.
112 id: The id of the developer to show in the result.
113 startfrom: The commit to start logging from.
114 relative: Treat path as relative to the current working directory.
117 git = Repo(".").git
119 result = git.log(start_from, "--", path, pretty="format:%" + id, with_keep_cwd=relative)
120 activity = result.split('\n')
122 result = {}
124 for line in activity:
125 # Create an entry if there was none for this author yet
126 if not result.has_key(line):
127 result[line] = 0
129 result[line] += 1
131 return result
133 def activity(id, field, startFrom):
134 """Shows the activity for the specified developer in the current repo.
136 Params:
137 id: The id of the developer, as specified by field.
138 field: The field to filter on.
139 startfrom: The commit to start logging from.
142 git = Repo(".").git
143 result = git.log("--numstat", startFrom, "--", pretty="format:%" + field)
145 log = result.splitlines(True)
146 activity = activityInArea(log, id)
148 result = []
150 for key, value in activity.iteritems():
151 result.append(str(key) + " = " + str(value))
153 return result
155 def aggregateActivity(idFilter, field, startFrom):
156 """Aggregates the activity for all developers
158 Args:
159 idFilter: The id to filter on, if None all developers will be shown.
160 field: The field to filter on.
163 result = []
165 # TODO, implement
167 return result
169 def _checkOptions(parser, options):
170 """Checks the specified options and uses the parser to indicate errors
172 Args:
173 parser: The parser to use to signal when the options are bad.
174 options: The options to check.
177 opts = [options.aggregate, options.developer, options.file]
179 if not parse.isUnique(opts):
180 parser.error("Please choose exactly one mode")
183 if options.file:
184 try:
185 parse.check_file(value=options.file, relative=options.relative)
186 except OptionValueError, e:
187 parser.error(e)
190 def dispatch(*args):
191 """Dispatches author related commands
194 progname = os.path.basename(sys.argv[0]) + " author"
196 parser = OptionParser(option_class=parse.GitOption, prog=progname)
198 parser.add_option(
199 "-a", "--aggregate",
200 action="store_true",
201 help="aggregate the results")
203 parser.add_option(
204 "-d", "--developer",
205 help="the id to filter on")
207 parser.add_option(
208 "-f", "--file",
209 help="the file to filter on")
211 parser.add_option(
212 "-i", "--id",
213 help="the one/two letter identifier specifying which field to use as id")
215 parser.add_option(
216 "-s", "--start-from",
217 type="commit",
218 metavar="COMMIT",
219 help="the commit to start logging from")
221 parser.add_option(
222 "-r", "--relative",
223 action="store_true",
224 help="paths are relative to the current directory")
226 parser.set_default("id", "ae")
227 parser.set_default("start_from", "HEAD")
228 parser.set_default("relative", False)
230 (options, args) = parser.parse_args(list(args))
232 _checkOptions(parser, options)
234 if options.aggregate:
235 result = aggregateActivity(options.developer, options.id, options.start_from)
236 elif options.developer:
237 result = activity(options.developer, options.id, options.start_from)
238 elif options.file:
239 activity_for_file = activityInFile( options.file,
240 options.id,
241 options.start_from,
242 options.relative)
244 result = []
246 for key, value in activity_for_file.iteritems():
247 result.append(key + ": " + str(value))
249 for line in result:
250 print(line)