]> gitweb.ps.run Git - ps-cgit/blob - ui-stats.c
ui-stats.c: reuse cgit_add_hidden_formfields()
[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 static void add_commit(struct string_list *authors, struct commit *commit,
158         struct cgit_period *period)
159 {
160         struct commitinfo *info;
161         struct string_list_item *author, *item;
162         struct authorstat *authorstat;
163         struct string_list *items;
164         char *tmp;
165         struct tm *date;
166         time_t t;
167
168         info = cgit_parse_commit(commit);
169         tmp = xstrdup(info->author);
170         author = string_list_insert(tmp, authors);
171         if (!author->util)
172                 author->util = xcalloc(1, sizeof(struct authorstat));
173         else
174                 free(tmp);
175         authorstat = author->util;
176         items = &authorstat->list;
177         t = info->committer_date;
178         date = gmtime(&t);
179         period->trunc(date);
180         tmp = xstrdup(period->pretty(date));
181         item = string_list_insert(tmp, items);
182         if (item->util)
183                 free(tmp);
184         item->util++;
185         authorstat->total++;
186         cgit_free_commitinfo(info);
187 }
188
189 static int cmp_total_commits(const void *a1, const void *a2)
190 {
191         const struct string_list_item *i1 = a1;
192         const struct string_list_item *i2 = a2;
193         const struct authorstat *auth1 = i1->util;
194         const struct authorstat *auth2 = i2->util;
195
196         return auth2->total - auth1->total;
197 }
198
199 /* Walk the commit DAG and collect number of commits per author per
200  * timeperiod into a nested string_list collection.
201  */
202 struct string_list collect_stats(struct cgit_context *ctx,
203         struct cgit_period *period)
204 {
205         struct string_list authors;
206         struct rev_info rev;
207         struct commit *commit;
208         const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
209         int argc = 3;
210         time_t now;
211         long i;
212         struct tm *tm;
213         char tmp[11];
214
215         time(&now);
216         tm = gmtime(&now);
217         period->trunc(tm);
218         for (i = 1; i < period->count; i++)
219                 period->dec(tm);
220         strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
221         argv[2] = xstrdup(fmt("--since=%s", tmp));
222         if (ctx->qry.path) {
223                 argv[3] = "--";
224                 argv[4] = ctx->qry.path;
225                 argc += 2;
226         }
227         init_revisions(&rev, NULL);
228         rev.abbrev = DEFAULT_ABBREV;
229         rev.commit_format = CMIT_FMT_DEFAULT;
230         rev.no_merges = 1;
231         rev.verbose_header = 1;
232         rev.show_root_diff = 0;
233         setup_revisions(argc, argv, &rev, NULL);
234         prepare_revision_walk(&rev);
235         memset(&authors, 0, sizeof(authors));
236         while ((commit = get_revision(&rev)) != NULL) {
237                 add_commit(&authors, commit, period);
238                 free(commit->buffer);
239                 free_commit_list(commit->parents);
240         }
241         return authors;
242 }
243
244 void print_combined_authorrow(struct string_list *authors, int from, int to,
245         const char *name, const char *leftclass, const char *centerclass,
246         const char *rightclass, struct cgit_period *period)
247 {
248         struct string_list_item *author;
249         struct authorstat *authorstat;
250         struct string_list *items;
251         struct string_list_item *date;
252         time_t now;
253         long i, j, total, subtotal;
254         struct tm *tm;
255         char *tmp;
256
257         time(&now);
258         tm = gmtime(&now);
259         period->trunc(tm);
260         for (i = 1; i < period->count; i++)
261                 period->dec(tm);
262
263         total = 0;
264         htmlf("<tr><td class='%s'>%s</td>", leftclass,
265                 fmt(name, to - from + 1));
266         for (j = 0; j < period->count; j++) {
267                 tmp = period->pretty(tm);
268                 period->inc(tm);
269                 subtotal = 0;
270                 for (i = from; i <= to; i++) {
271                         author = &authors->items[i];
272                         authorstat = author->util;
273                         items = &authorstat->list;
274                         date = string_list_lookup(tmp, items);
275                         if (date)
276                                 subtotal += (size_t)date->util;
277                 }
278                 htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
279                 total += subtotal;
280         }
281         htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
282 }
283
284 void print_authors(struct string_list *authors, int top,
285                    struct cgit_period *period)
286 {
287         struct string_list_item *author;
288         struct authorstat *authorstat;
289         struct string_list *items;
290         struct string_list_item *date;
291         time_t now;
292         long i, j, total;
293         struct tm *tm;
294         char *tmp;
295
296         time(&now);
297         tm = gmtime(&now);
298         period->trunc(tm);
299         for (i = 1; i < period->count; i++)
300                 period->dec(tm);
301
302         html("<table class='stats'><tr><th>Author</th>");
303         for (j = 0; j < period->count; j++) {
304                 tmp = period->pretty(tm);
305                 htmlf("<th>%s</th>", tmp);
306                 period->inc(tm);
307         }
308         html("<th>Total</th></tr>\n");
309
310         if (top <= 0 || top > authors->nr)
311                 top = authors->nr;
312
313         for (i = 0; i < top; i++) {
314                 author = &authors->items[i];
315                 html("<tr><td class='left'>");
316                 html_txt(author->string);
317                 html("</td>");
318                 authorstat = author->util;
319                 items = &authorstat->list;
320                 total = 0;
321                 for (j = 0; j < period->count; j++)
322                         period->dec(tm);
323                 for (j = 0; j < period->count; j++) {
324                         tmp = period->pretty(tm);
325                         period->inc(tm);
326                         date = string_list_lookup(tmp, items);
327                         if (!date)
328                                 html("<td>0</td>");
329                         else {
330                                 htmlf("<td>%d</td>", date->util);
331                                 total += (size_t)date->util;
332                         }
333                 }
334                 htmlf("<td class='sum'>%d</td></tr>", total);
335         }
336
337         if (top < authors->nr)
338                 print_combined_authorrow(authors, top, authors->nr - 1,
339                         "Others (%d)", "left", "", "sum", period);
340
341         print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
342                 "total", "sum", "sum", period);
343         html("</table>");
344 }
345
346 /* Create a sorted string_list with one entry per author. The util-field
347  * for each author is another string_list which is used to calculate the
348  * number of commits per time-interval.
349  */
350 void cgit_show_stats(struct cgit_context *ctx)
351 {
352         struct string_list authors;
353         struct cgit_period *period;
354         int top, i;
355         const char *code = "w";
356
357         if (ctx->qry.period)
358                 code = ctx->qry.period;
359
360         i = cgit_find_stats_period(code, &period);
361         if (!i) {
362                 cgit_print_error(fmt("Unknown statistics type: %c", code));
363                 return;
364         }
365         if (i > ctx->repo->max_stats) {
366                 cgit_print_error(fmt("Statistics type disabled: %s",
367                                      period->name));
368                 return;
369         }
370         authors = collect_stats(ctx, period);
371         qsort(authors.items, authors.nr, sizeof(struct string_list_item),
372                 cmp_total_commits);
373
374         top = ctx->qry.ofs;
375         if (!top)
376                 top = 10;
377         htmlf("<h2>Commits per author per %s", period->name);
378         if (ctx->qry.path) {
379                 html(" (path '");
380                 html_txt(ctx->qry.path);
381                 html("')");
382         }
383         html("</h2>");
384
385         html("<form method='get' action='' style='float: right; text-align: right;'>");
386         cgit_add_hidden_formfields(1, 0, "stats");
387         if (ctx->repo->max_stats > 1) {
388                 html("Period: ");
389                 html("<select name='period' onchange='this.form.submit();'>");
390                 for (i = 0; i < ctx->repo->max_stats; i++)
391                         htmlf("<option value='%c'%s>%s</option>",
392                                 periods[i].code,
393                                 period == &periods[i] ? " selected" : "",
394                                 periods[i].name);
395                 html("</select><br/><br/>");
396         }
397         html("Authors: ");
398         html("");
399         html("<select name='ofs' onchange='this.form.submit();'>");
400         htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
401         htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
402         htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
403         htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
404         htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
405         html("</select>");
406         html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
407         html("</form>");
408         print_authors(&authors, top, period);
409 }
410