]> gitweb.ps.run Git - ps-cgit/blob - ui-stats.c
Add support for 'enable-gitweb-owner' option
[ps-cgit] / ui-stats.c
1 #include <string-list.h>
2
3 #include "cgit.h"
4 #include "html.h"
5 #include "ui-shared.h"
6 #include "ui-stats.h"
7
8 #define MONTHS 6
9
10 struct authorstat {
11         long total;
12         struct string_list list;
13 };
14
15 #define DAY_SECS (60 * 60 * 24)
16 #define WEEK_SECS (DAY_SECS * 7)
17
18 static void trunc_week(struct tm *tm)
19 {
20         time_t t = timegm(tm);
21         t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
22         gmtime_r(&t, tm);       
23 }
24
25 static void dec_week(struct tm *tm)
26 {
27         time_t t = timegm(tm);
28         t -= WEEK_SECS;
29         gmtime_r(&t, tm);       
30 }
31
32 static void inc_week(struct tm *tm)
33 {
34         time_t t = timegm(tm);
35         t += WEEK_SECS;
36         gmtime_r(&t, tm);       
37 }
38
39 static char *pretty_week(struct tm *tm)
40 {
41         static char buf[10];
42
43         strftime(buf, sizeof(buf), "W%V %G", tm);
44         return buf;
45 }
46
47 static void trunc_month(struct tm *tm)
48 {
49         tm->tm_mday = 1;
50 }
51
52 static void dec_month(struct tm *tm)
53 {
54         tm->tm_mon--;
55         if (tm->tm_mon < 0) {
56                 tm->tm_year--;
57                 tm->tm_mon = 11;
58         }
59 }
60
61 static void inc_month(struct tm *tm)
62 {
63         tm->tm_mon++;
64         if (tm->tm_mon > 11) {
65                 tm->tm_year++;
66                 tm->tm_mon = 0;
67         }
68 }
69
70 static char *pretty_month(struct tm *tm)
71 {
72         static const char *months[] = {
73                 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
74                 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
75         };
76         return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
77 }
78
79 static void trunc_quarter(struct tm *tm)
80 {
81         trunc_month(tm);
82         while(tm->tm_mon % 3 != 0)
83                 dec_month(tm);
84 }
85
86 static void dec_quarter(struct tm *tm)
87 {
88         dec_month(tm);
89         dec_month(tm);
90         dec_month(tm);
91 }
92
93 static void inc_quarter(struct tm *tm)
94 {
95         inc_month(tm);
96         inc_month(tm);
97         inc_month(tm);
98 }
99
100 static char *pretty_quarter(struct tm *tm)
101 {
102         return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
103 }
104
105 static void trunc_year(struct tm *tm)
106 {
107         trunc_month(tm);
108         tm->tm_mon = 0;
109 }
110
111 static void dec_year(struct tm *tm)
112 {
113         tm->tm_year--;
114 }
115
116 static void inc_year(struct tm *tm)
117 {
118         tm->tm_year++;
119 }
120
121 static char *pretty_year(struct tm *tm)
122 {
123         return fmt("%d", tm->tm_year + 1900);
124 }
125
126 struct cgit_period periods[] = {
127         {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
128         {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
129         {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
130         {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
131 };
132
133 /* Given a period code or name, return a period index (1, 2, 3 or 4)
134  * and update the period pointer to the correcsponding struct.
135  * If no matching code is found, return 0.
136  */
137 int cgit_find_stats_period(const char *expr, struct cgit_period **period)
138 {
139         int i;
140         char code = '\0';
141
142         if (!expr)
143                 return 0;
144
145         if (strlen(expr) == 1)
146                 code = expr[0];
147
148         for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
149                 if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
150                         if (period)
151                                 *period = &periods[i];
152                         return i+1;
153                 }
154         return 0;
155 }
156
157 const char *cgit_find_stats_periodname(int idx)
158 {
159         if (idx > 0 && idx < 4)
160                 return periods[idx - 1].name;
161         else
162                 return "";
163 }
164
165 static void add_commit(struct string_list *authors, struct commit *commit,
166         struct cgit_period *period)
167 {
168         struct commitinfo *info;
169         struct string_list_item *author, *item;
170         struct authorstat *authorstat;
171         struct string_list *items;
172         char *tmp;
173         struct tm *date;
174         time_t t;
175
176         info = cgit_parse_commit(commit);
177         tmp = xstrdup(info->author);
178         author = string_list_insert(tmp, authors);
179         if (!author->util)
180                 author->util = xcalloc(1, sizeof(struct authorstat));
181         else
182                 free(tmp);
183         authorstat = author->util;
184         items = &authorstat->list;
185         t = info->committer_date;
186         date = gmtime(&t);
187         period->trunc(date);
188         tmp = xstrdup(period->pretty(date));
189         item = string_list_insert(tmp, items);
190         if (item->util)
191                 free(tmp);
192         item->util++;
193         authorstat->total++;
194         cgit_free_commitinfo(info);
195 }
196
197 static int cmp_total_commits(const void *a1, const void *a2)
198 {
199         const struct string_list_item *i1 = a1;
200         const struct string_list_item *i2 = a2;
201         const struct authorstat *auth1 = i1->util;
202         const struct authorstat *auth2 = i2->util;
203
204         return auth2->total - auth1->total;
205 }
206
207 /* Walk the commit DAG and collect number of commits per author per
208  * timeperiod into a nested string_list collection.
209  */
210 struct string_list collect_stats(struct cgit_context *ctx,
211         struct cgit_period *period)
212 {
213         struct string_list authors;
214         struct rev_info rev;
215         struct commit *commit;
216         const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
217         int argc = 3;
218         time_t now;
219         long i;
220         struct tm *tm;
221         char tmp[11];
222
223         time(&now);
224         tm = gmtime(&now);
225         period->trunc(tm);
226         for (i = 1; i < period->count; i++)
227                 period->dec(tm);
228         strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
229         argv[2] = xstrdup(fmt("--since=%s", tmp));
230         if (ctx->qry.path) {
231                 argv[3] = "--";
232                 argv[4] = ctx->qry.path;
233                 argc += 2;
234         }
235         init_revisions(&rev, NULL);
236         rev.abbrev = DEFAULT_ABBREV;
237         rev.commit_format = CMIT_FMT_DEFAULT;
238         rev.no_merges = 1;
239         rev.verbose_header = 1;
240         rev.show_root_diff = 0;
241         setup_revisions(argc, argv, &rev, NULL);
242         prepare_revision_walk(&rev);
243         memset(&authors, 0, sizeof(authors));
244         while ((commit = get_revision(&rev)) != NULL) {
245                 add_commit(&authors, commit, period);
246                 free(commit->buffer);
247                 free_commit_list(commit->parents);
248         }
249         return authors;
250 }
251
252 void print_combined_authorrow(struct string_list *authors, int from, int to,
253         const char *name, const char *leftclass, const char *centerclass,
254         const char *rightclass, struct cgit_period *period)
255 {
256         struct string_list_item *author;
257         struct authorstat *authorstat;
258         struct string_list *items;
259         struct string_list_item *date;
260         time_t now;
261         long i, j, total, subtotal;
262         struct tm *tm;
263         char *tmp;
264
265         time(&now);
266         tm = gmtime(&now);
267         period->trunc(tm);
268         for (i = 1; i < period->count; i++)
269                 period->dec(tm);
270
271         total = 0;
272         htmlf("<tr><td class='%s'>%s</td>", leftclass,
273                 fmt(name, to - from + 1));
274         for (j = 0; j < period->count; j++) {
275                 tmp = period->pretty(tm);
276                 period->inc(tm);
277                 subtotal = 0;
278                 for (i = from; i <= to; i++) {
279                         author = &authors->items[i];
280                         authorstat = author->util;
281                         items = &authorstat->list;
282                         date = string_list_lookup(tmp, items);
283                         if (date)
284                                 subtotal += (size_t)date->util;
285                 }
286                 htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
287                 total += subtotal;
288         }
289         htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
290 }
291
292 void print_authors(struct string_list *authors, int top,
293                    struct cgit_period *period)
294 {
295         struct string_list_item *author;
296         struct authorstat *authorstat;
297         struct string_list *items;
298         struct string_list_item *date;
299         time_t now;
300         long i, j, total;
301         struct tm *tm;
302         char *tmp;
303
304         time(&now);
305         tm = gmtime(&now);
306         period->trunc(tm);
307         for (i = 1; i < period->count; i++)
308                 period->dec(tm);
309
310         html("<table class='stats'><tr><th>Author</th>");
311         for (j = 0; j < period->count; j++) {
312                 tmp = period->pretty(tm);
313                 htmlf("<th>%s</th>", tmp);
314                 period->inc(tm);
315         }
316         html("<th>Total</th></tr>\n");
317
318         if (top <= 0 || top > authors->nr)
319                 top = authors->nr;
320
321         for (i = 0; i < top; i++) {
322                 author = &authors->items[i];
323                 html("<tr><td class='left'>");
324                 html_txt(author->string);
325                 html("</td>");
326                 authorstat = author->util;
327                 items = &authorstat->list;
328                 total = 0;
329                 for (j = 0; j < period->count; j++)
330                         period->dec(tm);
331                 for (j = 0; j < period->count; j++) {
332                         tmp = period->pretty(tm);
333                         period->inc(tm);
334                         date = string_list_lookup(tmp, items);
335                         if (!date)
336                                 html("<td>0</td>");
337                         else {
338                                 htmlf("<td>%d</td>", date->util);
339                                 total += (size_t)date->util;
340                         }
341                 }
342                 htmlf("<td class='sum'>%d</td></tr>", total);
343         }
344
345         if (top < authors->nr)
346                 print_combined_authorrow(authors, top, authors->nr - 1,
347                         "Others (%d)", "left", "", "sum", period);
348
349         print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
350                 "total", "sum", "sum", period);
351         html("</table>");
352 }
353
354 /* Create a sorted string_list with one entry per author. The util-field
355  * for each author is another string_list which is used to calculate the
356  * number of commits per time-interval.
357  */
358 void cgit_show_stats(struct cgit_context *ctx)
359 {
360         struct string_list authors;
361         struct cgit_period *period;
362         int top, i;
363         const char *code = "w";
364
365         if (ctx->qry.period)
366                 code = ctx->qry.period;
367
368         i = cgit_find_stats_period(code, &period);
369         if (!i) {
370                 cgit_print_error(fmt("Unknown statistics type: %c", code));
371                 return;
372         }
373         if (i > ctx->repo->max_stats) {
374                 cgit_print_error(fmt("Statistics type disabled: %s",
375                                      period->name));
376                 return;
377         }
378         authors = collect_stats(ctx, period);
379         qsort(authors.items, authors.nr, sizeof(struct string_list_item),
380                 cmp_total_commits);
381
382         top = ctx->qry.ofs;
383         if (!top)
384                 top = 10;
385         htmlf("<h2>Commits per author per %s", period->name);
386         if (ctx->qry.path) {
387                 html(" (path '");
388                 html_txt(ctx->qry.path);
389                 html("')");
390         }
391         html("</h2>");
392
393         html("<form method='get' action='' style='float: right; text-align: right;'>");
394         cgit_add_hidden_formfields(1, 0, "stats");
395         if (ctx->repo->max_stats > 1) {
396                 html("Period: ");
397                 html("<select name='period' onchange='this.form.submit();'>");
398                 for (i = 0; i < ctx->repo->max_stats; i++)
399                         htmlf("<option value='%c'%s>%s</option>",
400                                 periods[i].code,
401                                 period == &periods[i] ? " selected" : "",
402                                 periods[i].name);
403                 html("</select><br/><br/>");
404         }
405         html("Authors: ");
406         html("");
407         html("<select name='ofs' onchange='this.form.submit();'>");
408         htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
409         htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
410         htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
411         htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
412         htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
413         html("</select>");
414         html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
415         html("</form>");
416         print_authors(&authors, top, period);
417 }
418