6 from optparse
import OptionParser
, OptionValueError
9 from git_stats
import parse
12 """Simple storage class containing stats on the activity in one file."""
21 return "%4d: %5d+ %5d- %5d~" % \
22 (self
.count
, self
.added
, self
.deleted
, self
.added
-self
.deleted
)
24 def activityInArea(log
):
25 """Parses the specified file containing commit logs.
26 The output is expected to be in the format described below:
29 [<lines added>\t<lines deleted>\t<path>]+
34 log: The log formatted as described above.
37 A dictionary containing activity per author is returned.
38 Each author contains a dictionary with activity per path.
39 Each path contains one Activity object with the aggregated results.
42 # Create a dict to store the activity stats per path
45 # Create a place to store the result in
51 # Parse all the lines in the file
55 # Split the line at the tab and store the data
56 splitline
= line
.split('\t')
58 length
= len(line
.lstrip())
60 # There is something on this line, but it contains no separator
61 if size
== 1 and length
> 0:
62 # Get the id address minus the newline
69 addpart
= splitline
[0]
70 deletepart
= splitline
[1]
78 activity
.added
= int(addpart
)
79 activity
.deleted
= int(deletepart
)
81 print("On line '" + str(i
) + "', could not convert number: " + str(e
))
83 activity
.path
= splitline
[2][:-1]
84 activities
.append(activity
)
86 for activity
in activities
:
87 for author
in activity
.id:
88 if not author
in activityByAuthor
:
89 activityByAuthor
[author
] = {}
91 activityByPath
= activityByAuthor
[author
]
93 # If we have not encountered this path, create an entry
94 if not activity
.path
in activityByPath
:
96 activityByPath
[activity
.path
] = addme
98 known
= activityByPath
[activity
.path
]
101 result
.added
= known
.added
+ activity
.added
102 result
.deleted
= known
.deleted
+ activity
.deleted
103 result
.count
= known
.count
+ 1
105 # Store it under it's path
106 activityByPath
[activity
.path
] = result
108 # Store it under it's author
109 activityByAuthor
[author
] = activityByPath
111 # Create a fresh activity to store the next round in
115 print("Cannot parse line " + str(i
) + ".")
118 return activityByAuthor
120 def activityInFile(path
, id, start_from
, relative
):
121 """Shows the activity for the file in the current repo.
124 path: The path to filter on.
125 id: The id of the developer to show in the result.
126 startfrom: The commit to start logging from.
127 relative: Treat path as relative to the current working directory.
132 result
= git
.log(start_from
, "--", path
, pretty
="format:%" + id, with_keep_cwd
=relative
)
133 activity
= result
.split('\n')
137 for line
in activity
:
138 # Create an entry if there was none for this author yet
139 if not line
in result
:
146 def activity(id, field
, startFrom
):
147 """Shows the activity for the specified developer in the current repo.
150 id: The id of the developer, as specified by field.
151 field: The field to filter on.
152 startfrom: The commit to start logging from.
156 result
= git
.log("--numstat", startFrom
, "--", pretty
="format:%" + field
)
158 log
= result
.splitlines(True)
159 allActivity
= activityInArea(log
)
164 for author
in sorted(allActivity
):
165 activity
= allActivity
[author
]
166 result
.append(author
+ ":")
168 for key
in sorted(activity
):
169 value
= activity
[key
]
170 result
.append("\t" + str(value
) + " = " + str(key
))
176 if not id in allActivity
:
177 result
.append("Unknown author " + id)
178 result
.append("Known authors:")
179 result
.extend(allActivity
.keys())
183 activity
= allActivity
[id]
185 for key
in sorted(activity
):
186 value
= activity
[key
]
187 result
.append(str(value
) + " = " + str(key
))
191 def aggregateActivity(idFilter
, field
, startFrom
):
192 """Aggregates the activity for all developers
195 idFilter: The id to filter on, if None all developers will be shown.
196 field: The field to filter on.
200 result
= git
.log("--numstat", startFrom
, "--", pretty
="format:%" + field
)
202 log
= result
.splitlines(True)
203 allActivity
= activityInArea(log
)
205 aggregatedActivity
= {}
207 for _
, activityByPath
in allActivity
.iteritems():
208 for path
, activity
in activityByPath
.iteritems():
209 if not path
in aggregatedActivity
:
210 aggregatedActivity
[path
] = Activity()
212 known
= aggregatedActivity
[path
]
215 result
.added
= known
.added
+ activity
.added
216 result
.deleted
= known
.deleted
+ activity
.deleted
217 result
.count
= known
.count
+ activity
.count
219 aggregatedActivity
[path
] = result
223 for key
in sorted(aggregatedActivity
):
224 value
= aggregatedActivity
[key
]
225 result
.append(str(value
) + " = " + str(key
))
229 def _checkOptions(parser
, options
):
230 """Checks the specified options and uses the parser to indicate errors
233 parser: The parser to use to signal when the options are bad.
234 options: The options to check.
237 opts
= [options
.aggregate
, options
.developer
, options
.file, options
.everyone
]
239 if not parse
.isUnique(opts
, atLeastOne
=True):
240 parser
.error("Please choose exactly one mode")
244 parse
.check_file(value
=options
.file, relative
=options
.relative
)
245 except OptionValueError
, e
:
250 """Dispatches author related commands
253 progname
= os
.path
.basename(sys
.argv
[0]) + " author"
255 parser
= OptionParser(option_class
=parse
.GitOption
, prog
=progname
)
260 help="aggregate the results")
265 help="show the activity of all developers")
269 help="the id to filter on")
273 help="the file to filter on")
277 help="the one/two letter identifier specifying which field to use as id")
280 "-s", "--start-from",
283 help="the commit to start logging from")
288 help="paths are relative to the current directory")
290 parser
.set_default("id", "ae")
291 parser
.set_default("start_from", "HEAD")
292 parser
.set_default("relative", False)
294 (options
, args
) = parser
.parse_args(list(args
))
296 _checkOptions(parser
, options
)
298 if options
.aggregate
:
299 result
= aggregateActivity(options
.developer
, options
.id, options
.start_from
)
300 elif options
.developer
or options
.everyone
:
301 result
= activity(options
.developer
, options
.id, options
.start_from
)
303 activity_for_file
= activityInFile( options
.file,
310 for key
, value
in activity_for_file
.iteritems():
311 result
.append(key
+ ": " + str(value
))