-
Notifications
You must be signed in to change notification settings - Fork 0
/
svnstats.groovy
290 lines (228 loc) · 8.33 KB
/
svnstats.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
import static Globals.*
import static LogFileProcessor.commitsByAuthorInLogFile
import static Statistics.commitStatisticsFor
/**
* Turns svn log XML output into some statistics.
* (Yes, TortoiseSVN does this out of the box and much better ...)
*
* Create a logfile using the 'svn log' command, e.g.
*
* <pre>
* svn log -v -r{2013-01-01}:head --xml > svn-log.xml
* </pre>
*
* The goal is to turn this ...
*
* <pre>
* <log>
* <logentry revision="23419">
* <author>mrx</author>
* <date>2013-11-30T19:34:11.280185Z</date>
* <paths>
* <path action="A" kind="file">
* /foo/src/main/resources/foo/Bar.groovy
* </path>
* </paths>
* <msg>Very important foo</msg>
* </logentry>
* </log>
* </pre>
*
* ... into this:
*
* <pre>
* -----------------------------------------------------------------------------------------------------------
* mrx
* 2013-01-01
* 09:00 (rev 2342: Very important foo)
* A file /foo/src/main/resources/foo/Bar.groovy
*
* -----------------------------------------------------------------------------------------------------------
*
* Statistics:
*
* Ordered by: Active Days
* -------------------------------------------------------------------------
* | Author | Active Days | Total Commits | Commits / Day |
* -------------------------------------------------------------------------
* | foo | 10 | 34 | 3 |
* | bar | 9 | 194 | 21 |
* </pre>
*/
if (args.size() < 1) {
println "Usage: svnstats svn-log-file [log]"
System.exit 1
}
// path to svn log file
// created by e.g.: svn log -v -r{2013-01-01}:head --xml > svn-log.xml)
svnlogfile = System.properties.'user.dir' + '/' + args[0]
if (args.size() == 2) {
Globals.doLog = 'log' == args[1].toLowerCase()
}
// committer filtering
// 'all' => do not filter
// knownCommitters = 'all' // [ 'foo', 'bar' ]
knownCommitters = [
'langeth', 'lohse', 'goedereis', 'skotnicki', 'huebner', 'dohse', 'stoldt', 'bohnhoff',
'fegebank', 'krusege', 'jarde', 'mohr', 'desch', 'floedl', 'gunsch', 'philipp', 'proebstlean',
'schaeferst', 'muellermi', 'scheffold', 'steidle', 'vishnu', 'taglieber'
]
// ugly, but a script generates a class with a main function on the fly and that's why ...
class Globals {
// set this to true for some noise
// showing the reformatted XML contents
static doLog = false
static log(text) { if (doLog) println text }
static line = '-' * 100
static PADDING = 15
static pad = { (it as String).padLeft(PADDING) }
static String toNumInUnits(long bytes) {
int u = 0
for (;bytes > 1024*1024; bytes >>= 10) u++
if (bytes > 1024) u++
String.format('%.1f %cB', bytes/1024f, ' KMGTPE'.charAt(u))
}
}
println "Processing $svnlogfile ..."
commitStatistics = commitStatisticsFor commitsByAuthorInLogFile(svnlogfile)
activeCommitters = null
unknownCommitters = null
// committer filtering
if (knownCommitters != 'all') {
activeCommitters = commitStatistics.inject([]) { acc, stat ->
if (knownCommitters.contains(stat.author)) {
acc << stat.author
}
acc
}
unknownCommitters = commitStatistics.inject([]) { acc, stat ->
if (!knownCommitters.contains(stat.author)) {
acc << stat.author
}
acc
}
commitStatistics.removeAll { !knownCommitters.contains(it.author) }
}
println ''
println line
println 'Svn Commit Statistics'
println "Based on $svnlogfile (${toNumInUnits(new File(svnlogfile).size())})"
println line
println ''
tableLine = ' ' + ('-' * (PADDING * 5 + 14))
columnNames = '| ' + ['Author', 'Active Days', 'Total Commits', 'Changes', 'Commits / Day'].collect { it.center(PADDING) } .join(' | ') + ' |'
sorting = [
'Active Days' : { it.daysWithCommits * -1 },
'Commits per Day' : { it.commitsPerDay * -1 },
'Total Commits' : { it.totalCommits * -1 },
'Changes' : { it.changes * -1 }
]
sorting.each { title, order ->
println "Ordered by: $title"
println tableLine
println columnNames
println tableLine
commitStatistics.sort(order).each { println it }
println tableLine + '\n'
}
if (knownCommitters != 'all') {
println "Known committers:\n\t${knownCommitters.sort().join(', ')}\n"
println "Active committers:\n\t${activeCommitters.sort().join(', ')}\n"
println "Unknown committers:\n\t${unknownCommitters.sort().join(', ')}\n"
}
// log file processing and statistics generation namespaces -------------------------------------------------------
class LogFileProcessor {
static Map commitsByAuthorInLogFile(String svnlogfile) {
final commitsByAuthor = [:]
final logEntries = new XmlSlurper().parse(svnlogfile).logentry
logEntries.each { logEntry ->
String author = logEntry.author
final entryForAuthor = commitsByAuthor[author]
if (entryForAuthor == null) {
entryForAuthor = [:]
commitsByAuthor[author] = entryForAuthor
}
String revision = logEntry.'@revision'
String message = str(logEntry.msg).replaceAll('\\n', ' ')
DateTime dateTime = DateTime.parseFrom(logEntry.date)
final changes = logEntry.paths.path.collect { path ->
String action = str(path.'@action') + ' ' + str(path.'@kind')
String value = str(path)
"$action: $value"
}
final activityForDate = entryForAuthor[dateTime.date]
if (activityForDate == null) {
activityForDate = [:]
entryForAuthor[dateTime.date] = activityForDate
}
activityForDate[dateTime.time] = [
revision : revision,
changes : changes,
message : message ?: '*** COMMIT MESSAGE MISSING ***'
]
}
commitsByAuthor
}
static String str(node) {
node as String
}
}
class Statistics {
String author
int daysWithCommits
int totalCommits
int changes
int getCommitsPerDay() { (daysWithCommits ? (totalCommits / daysWithCommits) : 0 ) as Integer }
@Override
String toString() {
'| ' + [author, daysWithCommits, totalCommits, changes, commitsPerDay].collect { pad it } .join(' | ') + ' |'
}
static commitStatisticsFor(Map commitsByAuthor) {
final commitStatistics = []
commitsByAuthor.each { author, entries ->
log line
log author
log line
final dates = entries.keySet().sort()
final stats = new Statistics(author : author)
stats.daysWithCommits = dates.size()
dates.each { date ->
final activitiesForDate = entries[date]
final timesOfDay = activitiesForDate.keySet().sort()
log "\t$date"
timesOfDay.each { timeOfDay ->
final activity = activitiesForDate[timeOfDay]
log "\t\t$timeOfDay (rev $activity.revision: $activity.message)"
stats.totalCommits += 1
stats.changes += activity.changes.size()
if (doLog) {
activity.changes.each { commit ->
log "\t\t\t$commit"
}
}
}
}
commitStatistics << stats
}
commitStatistics
}
}
class DateTime {
String date, time
static DateTime parseFrom(xmlDateTime) {
final javaDate = Date.parse(DATE_TIME_PATTERN, parsableDate(xmlDateTime))
new DateTime (
date : javaDate.format(DATE_PATTERN),
time : javaDate.format(TIME_PATTERN)
)
}
static String parsableDate(xmlDateTime) {
// Date.parse can't digest this: 2013-11-30T19:34:11.280185Z
// so remove the 'T' and cut off the tail after the seconds '.'
xmlDateTime = (xmlDateTime as String).replace('T', ' ')
xmlDateTime[0..<(xmlDateTime.indexOf('.'))]
}
static final DATE_PATTERN = 'yyyy-MM-dd'
static final TIME_PATTERN = 'HH:mm:ss'
static final DATE_TIME_PATTERN = DATE_PATTERN + ' ' + TIME_PATTERN
}