Fossil SCM

fossil-scm / src / statrep.c
Blame History Raw 974 lines
1
/*
2
** Copyright (c) 2013 Stephan Beal
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code to implement the /reports web page.
19
**
20
*/
21
#include "config.h"
22
#include <string.h>
23
#include <time.h>
24
#include "statrep.h"
25
26
27
/*
28
** Used by stats_report_xxxxx() to remember which type of events
29
** to show. Populated by stats_report_init_view() and holds the
30
** return value of that function.
31
*/
32
static int statsReportType = 0;
33
34
/*
35
** Set by stats_report_init_view() to one of the y=XXXX values
36
** accepted by /timeline?y=XXXX.
37
*/
38
static const char *statsReportTimelineYFlag = NULL;
39
40
41
/*
42
** Creates a TEMP VIEW named v_reports which is a wrapper around the
43
** EVENT table filtered on event.type. It looks for the request
44
** parameter 'type' (reminder: we "should" use 'y' for consistency
45
** with /timeline, but /reports uses 'y' for the year) and expects it
46
** to contain one of the conventional values from event.type or the
47
** value "all", which is treated as equivalent to "*". By default (if
48
** no 'y' is specified), "*" is assumed (that is also the default for
49
** invalid/unknown filter values). That 'y' filter is the one used for
50
** the event list. Note that a filter of "*" or "all" is equivalent to
51
** querying against the full event table. The view, however, adds an
52
** abstraction level to simplify the implementation code for the
53
** various /reports pages.
54
**
55
** Returns one of: 'c', 'f', 'w', 'g', 't', 'e', representing the type of
56
** filter it applies, or '*' if no filter is applied (i.e. if "all" is
57
** used).
58
*/
59
static int stats_report_init_view(){
60
const char *zType = PD("type","*"); /* analog to /timeline?y=... */
61
const char *zRealType = NULL; /* normalized form of zType */
62
int rc = 0; /* result code */
63
char *zTimeSpan; /* Time span */
64
assert( !statsReportType && "Must not be called more than once." );
65
switch( (zType && *zType) ? *zType : 0 ){
66
case 'c':
67
case 'C':
68
zRealType = "ci";
69
rc = *zRealType;
70
break;
71
case 'e':
72
case 'E':
73
zRealType = "e";
74
rc = *zRealType;
75
break;
76
case 'f':
77
case 'F':
78
zRealType = "f";
79
rc = *zRealType;
80
break;
81
case 'g':
82
case 'G':
83
zRealType = "g";
84
rc = *zRealType;
85
break;
86
case 'm':
87
case 'M':
88
zRealType = "m";
89
rc = *zRealType;
90
break;
91
case 'n':
92
case 'N':
93
zRealType = "n";
94
rc = *zRealType;
95
break;
96
case 't':
97
case 'T':
98
zRealType = "t";
99
rc = *zRealType;
100
break;
101
case 'w':
102
case 'W':
103
zRealType = "w";
104
rc = *zRealType;
105
break;
106
default:
107
rc = '*';
108
break;
109
}
110
assert(0 != rc);
111
if( P("from")!=0 && P("to")!=0 ){
112
zTimeSpan = mprintf(
113
" (event.mtime BETWEEN julianday(%Q) AND julianday(%Q))",
114
P("from"), P("to"));
115
}else{
116
zTimeSpan = " 1";
117
}
118
if( zRealType==0 ){
119
statsReportTimelineYFlag = "a";
120
db_multi_exec("CREATE TEMP VIEW v_reports AS "
121
"SELECT * FROM event WHERE %s", zTimeSpan/*safe-for-%s*/);
122
}else if( rc!='n' && rc!='m' ){
123
statsReportTimelineYFlag = zRealType;
124
db_multi_exec("CREATE TEMP VIEW v_reports AS "
125
"SELECT * FROM event WHERE (type GLOB %Q) AND %s",
126
zRealType, zTimeSpan/*safe-for-%s*/);
127
}else{
128
const char *zNot = rc=='n' ? "NOT" : "";
129
statsReportTimelineYFlag = "ci";
130
db_multi_exec(
131
"CREATE TEMP VIEW v_reports AS "
132
"SELECT * FROM event WHERE type='ci' AND %s"
133
" AND objid %s IN (SELECT cid FROM plink WHERE NOT isprim)",
134
zTimeSpan/*safe-for-%s*/, zNot/*safe-for-%s*/
135
);
136
}
137
return statsReportType = rc;
138
}
139
140
/*
141
** Returns a string suitable (for a given value of suitable) for
142
** use in a label with the header of the /reports pages, dependent
143
** on the 'type' flag. See stats_report_init_view().
144
** The returned bytes are static.
145
*/
146
static const char *stats_report_label_for_type(){
147
assert( statsReportType && "Must call stats_report_init_view() first." );
148
switch( statsReportType ){
149
case 'c':
150
return "check-ins";
151
case 'm':
152
return "merge check-ins";
153
case 'n':
154
return "non-merge check-ins";
155
case 'e':
156
return "technotes";
157
case 'f':
158
return "forum posts";
159
case 'w':
160
return "wiki changes";
161
case 't':
162
return "ticket changes";
163
case 'g':
164
return "tag changes";
165
default:
166
return "all types";
167
}
168
}
169
170
171
/*
172
** Implements the "byyear" and "bymonth" reports for /reports.
173
** If includeMonth is true then it generates the "bymonth" report,
174
** else the "byyear" report. If zUserName is not NULL then the report is
175
** restricted to events created by the named user account.
176
*/
177
static void stats_report_by_month_year(
178
char includeMonth, /* 0 for stats-by-year. 1 for stats-by-month */
179
const char *zUserName /* Only report events by this user */
180
){
181
Stmt query = empty_Stmt;
182
int nRowNumber = 0; /* current TR number */
183
int nEventTotal = 0; /* Total event count */
184
int rowClass = 0; /* counter for alternating
185
row colors */
186
const char *zTimeLabel = includeMonth ? "Year/Month" : "Year";
187
char zPrevYear[5] = {0}; /* For keeping track of when
188
we change years while looping */
189
int nEventsPerYear = 0; /* Total event count for the
190
current year */
191
char showYearTotal = 0; /* Flag telling us when to show
192
the per-year event totals */
193
int nMaxEvents = 1; /* for calculating length of graph
194
bars. */
195
int iterations = 0; /* number of weeks/months we iterate
196
over */
197
198
char *zCurrentTF; /* The timeframe in which 'now' lives */
199
double rNowFraction; /* Fraction of 'now' timeframe that has
200
passed */
201
int nTFChar; /* Prefix of date() for timeframe */
202
203
nTFChar = includeMonth ? 7 : 4;
204
stats_report_init_view();
205
db_prepare(&query,
206
"SELECT substr(date(mtime),1,%d) AS timeframe,"
207
" count(*) AS eventCount"
208
" FROM v_reports"
209
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
210
" GROUP BY timeframe"
211
" ORDER BY timeframe DESC",
212
nTFChar, zUserName);
213
@ <h1>Timeline Events (%s(stats_report_label_for_type()))
214
@ by year%s(includeMonth ? "/month" : "")
215
if( zUserName ){
216
@ for user %h(zUserName)
217
}
218
@ </h1>
219
@ <table border='0' cellpadding='2' cellspacing='0' \
220
zCurrentTF = db_text(0, "SELECT substr(date(),1,%d)", nTFChar);
221
if( !includeMonth ){
222
@ class='statistics-report-table-events sortable' \
223
@ data-column-types='tnx' data-init-sort='0'>
224
style_table_sorter();
225
rNowFraction = db_double(0.5,
226
"SELECT (unixepoch() - unixepoch('now','start of year'))*1.0/"
227
" (unixepoch('now','start of year','+1 year') - "
228
" unixepoch('now','start of year'));");
229
}else{
230
@ class='statistics-report-table-events'>
231
rNowFraction = db_double(0.5,
232
"SELECT (unixepoch() - unixepoch('now','start of month'))*1.0/"
233
" (unixepoch('now','start of month','+1 month') - "
234
" unixepoch('now','start of month'));");
235
}
236
@ <thead>
237
@ <th>%s(zTimeLabel)</th>
238
@ <th>Events</th>
239
@ <th width='90%%'><!-- relative commits graph --></th>
240
@ </thead><tbody>
241
/*
242
Run the query twice. The first time we calculate the maximum
243
number of events for a given row. Maybe someone with better SQL
244
Fu can re-implement this with a single query.
245
*/
246
while( SQLITE_ROW == db_step(&query) ){
247
int nCount = db_column_int(&query, 1);
248
if( strcmp(db_column_text(&query,0),zCurrentTF)==0
249
&& rNowFraction>0.05
250
){
251
nCount = (int)(((double)nCount)/rNowFraction);
252
}
253
if(nCount>nMaxEvents){
254
nMaxEvents = nCount;
255
}
256
++iterations;
257
}
258
db_reset(&query);
259
while( SQLITE_ROW == db_step(&query) ){
260
const char *zTimeframe = db_column_text(&query, 0);
261
const int nCount = db_column_int(&query, 1);
262
int nSize = (nCount>0 && nMaxEvents>0)
263
? (int)(100 * nCount / nMaxEvents)
264
: 1;
265
showYearTotal = 0;
266
if(!nSize) nSize = 1;
267
if(includeMonth){
268
/* For Month/year view, add a separator for each distinct year. */
269
if(!*zPrevYear ||
270
(0!=fossil_strncmp(zPrevYear,zTimeframe,4))){
271
showYearTotal = *zPrevYear;
272
if(showYearTotal){
273
rowClass = ++nRowNumber % 2;
274
@ <tr class='row%d(rowClass)'>
275
@ <td></td>
276
@ <td colspan='2'>Yearly total: %d(nEventsPerYear)</td>
277
@</tr>
278
showYearTotal = 0;
279
}
280
nEventsPerYear = 0;
281
memcpy(zPrevYear,zTimeframe,4);
282
rowClass = ++nRowNumber % 2;
283
@ <tr class='row%d(rowClass)'>
284
@ <th colspan='3' class='statistics-report-row-year'>%s(zPrevYear)</th>
285
@ </tr>
286
}
287
}
288
rowClass = ++nRowNumber % 2;
289
nEventTotal += nCount;
290
nEventsPerYear += nCount;
291
@<tr class='row%d(rowClass)'>
292
@ <td>
293
if(includeMonth){
294
cgi_printf("<a href='%R/timeline?"
295
"ym=%t&y=%s",
296
zTimeframe,
297
statsReportTimelineYFlag );
298
/* Reminder: n=nCount is not actually correct for bymonth unless
299
that was the only user who caused events.
300
*/
301
if( zUserName ){
302
cgi_printf("&u=%t", zUserName);
303
}
304
cgi_printf("' target='_new'>%s</a>",zTimeframe);
305
}else {
306
cgi_printf("<a href='?view=byweek&y=%s&type=%c",
307
zTimeframe, (char)statsReportType);
308
if( zUserName ){
309
cgi_printf("&u=%t", zUserName);
310
}
311
cgi_printf("'>%s</a>", zTimeframe);
312
}
313
@ </td><td>%d(nCount)</td>
314
@ <td style='white-space: nowrap;'>
315
if( strcmp(zTimeframe, zCurrentTF)==0
316
&& rNowFraction>0.05
317
&& nCount>0
318
&& nMaxEvents>0
319
){
320
/* If the timespan covered by this row contains "now", then project
321
** the number of changes until the completion of the timespan and
322
** show a dashed box of that projection. */
323
int nProj = (int)(((double)nCount)/rNowFraction);
324
int nExtra = (int)(((double)nCount)/rNowFraction) - nCount;
325
int nXSize = (100 * nExtra)/nMaxEvents;
326
@ <span class='statistics-report-graph-line' \
327
@ style='display:inline-block;min-width:%d(nSize)%%;'>&nbsp;</span>\
328
@ <span class='statistics-report-graph-extra' title='%d(nProj)' \
329
@ style='display:inline-block;min-width:%d(nXSize)%%;'>&nbsp;</span>\
330
}else{
331
@ <div class='statistics-report-graph-line' \
332
@ style='width:%d(nSize)%%;'>&nbsp;</div> \
333
}
334
@ </td>
335
@ </tr>
336
/*
337
Potential improvement: calculate the min/max event counts and
338
use percent-based graph bars.
339
*/
340
}
341
db_finalize(&query);
342
if(includeMonth && !showYearTotal && *zPrevYear){
343
/* Add final year total separator. */
344
rowClass = ++nRowNumber % 2;
345
@ <tr class='row%d(rowClass)'>
346
@ <td></td>
347
@ <td colspan='2'>Yearly total: %d(nEventsPerYear)</td>
348
@</tr>
349
}
350
@ </tbody></table>
351
if(nEventTotal){
352
const char *zAvgLabel = includeMonth ? "month" : "year";
353
int nAvg = iterations ? (nEventTotal/iterations) : 0;
354
@ <br><div>Total events: %d(nEventTotal)
355
@ <br>Average per active %s(zAvgLabel): %d(nAvg)
356
@ </div>
357
}
358
}
359
360
/*
361
** Implements the "byuser" view for /reports.
362
*/
363
static void stats_report_by_user(){
364
Stmt query = empty_Stmt;
365
int nRowNumber = 0; /* current TR number */
366
int rowClass = 0; /* counter for alternating
367
row colors */
368
int nMaxEvents = 1; /* max number of events for
369
all rows. */
370
stats_report_init_view();
371
@ <h1>Timeline Events
372
@ (%s(stats_report_label_for_type())) by User</h1>
373
db_multi_exec(
374
"CREATE TEMP VIEW piechart(amt,label) AS"
375
" SELECT count(*), ifnull(euser,user) FROM v_reports"
376
" GROUP BY ifnull(euser,user) ORDER BY count(*) DESC;"
377
);
378
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
379
@ <center><svg width=700 height=400>
380
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
381
@ </svg></centre><hr>
382
}
383
style_table_sorter();
384
@ <table class='statistics-report-table-events sortable' border='0' \
385
@ cellpadding='2' cellspacing='0' data-column-types='tkx' data-init-sort='2'>
386
@ <thead><tr>
387
@ <th>User</th>
388
@ <th>Events</th>
389
@ <th width='90%%'><!-- relative commits graph --></th>
390
@ </tr></thead><tbody>
391
db_prepare(&query,
392
"SELECT ifnull(euser,user), "
393
"COUNT(*) AS eventCount "
394
"FROM v_reports "
395
"GROUP BY ifnull(euser,user) ORDER BY eventCount DESC");
396
while( SQLITE_ROW == db_step(&query) ){
397
const int nCount = db_column_int(&query, 1);
398
if(nCount>nMaxEvents){
399
nMaxEvents = nCount;
400
}
401
}
402
db_reset(&query);
403
while( SQLITE_ROW == db_step(&query) ){
404
const char *zUser = db_column_text(&query, 0);
405
const int nCount = db_column_int(&query, 1);
406
char y = (char)statsReportType;
407
int nSize = nCount
408
? (int)(100 * nCount / nMaxEvents)
409
: 0;
410
if(!nCount) continue /* arguable! Possible? */;
411
else if(!nSize) nSize = 1;
412
rowClass = ++nRowNumber % 2;
413
@ <tr class='row%d(rowClass)'>
414
@ <td>
415
@ <a href="?view=bymonth&user=%h(zUser)&type=%c(y)">%h(zUser)</a>
416
@ </td><td data-sortkey='%08x(-nCount)'>%d(nCount)</td>
417
@ <td>
418
@ <div class='statistics-report-graph-line'
419
@ style='width:%d(nSize)%%;'>&nbsp;</div>
420
@ </td>
421
@</tr>
422
/*
423
Potential improvement: calculate the min/max event counts and
424
use percent-based graph bars.
425
*/
426
}
427
@ </tbody></table>
428
db_finalize(&query);
429
}
430
431
/*
432
** Implements the "byfile" view for /reports. If zUserName is not NULL then the
433
** report is restricted to events created by the named user account.
434
*/
435
static void stats_report_by_file(const char *zUserName){
436
Stmt query;
437
int mxEvent = 1; /* max number of events across all rows */
438
int nRowNumber = 0;
439
440
db_multi_exec(
441
"CREATE TEMP TABLE statrep(filename, cnt);"
442
"INSERT INTO statrep(filename, cnt)"
443
" SELECT filename.name, count(distinct mlink.mid)"
444
" FROM filename, mlink, event"
445
" WHERE filename.fnid=mlink.fnid"
446
" AND mlink.mid=event.objid"
447
" AND ifnull(coalesce(euser,user,'')=%Q,1)"
448
" GROUP BY 1", zUserName
449
);
450
db_prepare(&query,
451
"SELECT filename, cnt FROM statrep ORDER BY cnt DESC, filename /*sort*/"
452
);
453
mxEvent = db_int(1, "SELECT max(cnt) FROM statrep");
454
@ <h1>Check-ins Per File
455
if( zUserName ){
456
@ for user %h(zUserName)
457
}
458
@ </h1>
459
style_table_sorter();
460
@ <table class='statistics-report-table-events sortable' border='0' \
461
@ cellpadding='2' cellspacing='0' data-column-types='tNx' data-init-sort='2'>
462
@ <thead><tr>
463
@ <th>File</th>
464
@ <th>Check-ins</th>
465
@ <th width='90%%'><!-- relative commits graph --></th>
466
@ </tr></thead><tbody>
467
while( SQLITE_ROW == db_step(&query) ){
468
const char *zFile = db_column_text(&query, 0);
469
const int n = db_column_int(&query, 1);
470
int sz;
471
if( n<=0 ) continue;
472
sz = (int)(100*n/mxEvent);
473
if( sz==0 ) sz = 1;
474
@<tr class='row%d(++nRowNumber%2)'>
475
@ <td>%z(href("%R/finfo?name=%T",zFile))%h(zFile)</a></td>
476
@ <td>%d(n)</td>
477
@ <td>
478
@ <div class='statistics-report-graph-line'
479
@ style='width:%d(sz)%%;'>&nbsp;</div>
480
@ </td>
481
@</tr>
482
}
483
@ </tbody></table>
484
db_finalize(&query);
485
486
}
487
488
/*
489
** Implements the "byweekday" view for /reports. If zUserName is not NULL then
490
** the report is restricted to events created by the named user account.
491
*/
492
static void stats_report_day_of_week(const char *zUserName){
493
Stmt query = empty_Stmt;
494
int nRowNumber = 0; /* current TR number */
495
int rowClass = 0; /* counter for alternating
496
row colors */
497
int nMaxEvents = 1; /* max number of events for
498
all rows. */
499
static const char *const daysOfWeek[] = {
500
"Sunday", "Monday", "Tuesday", "Wednesday",
501
"Thursday", "Friday", "Saturday"
502
};
503
504
stats_report_init_view();
505
db_prepare(&query,
506
"SELECT cast(strftime('%%w', mtime) AS INTEGER) dow,"
507
" COUNT(*) AS eventCount"
508
" FROM v_reports"
509
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
510
" GROUP BY dow ORDER BY dow", zUserName);
511
@ <h1>Timeline Events (%h(stats_report_label_for_type())) by Day of the Week
512
if( zUserName ){
513
@ for user %h(zUserName)
514
}
515
@ </h1>
516
db_multi_exec(
517
"CREATE TEMP VIEW piechart(amt,label) AS"
518
" SELECT count(*),"
519
" CASE cast(strftime('%%w', mtime) AS INT)"
520
" WHEN 0 THEN 'Sunday'"
521
" WHEN 1 THEN 'Monday'"
522
" WHEN 2 THEN 'Tuesday'"
523
" WHEN 3 THEN 'Wednesday'"
524
" WHEN 4 THEN 'Thursday'"
525
" WHEN 5 THEN 'Friday'"
526
" WHEN 6 THEN 'Saturday'"
527
" ELSE 'ERROR'"
528
" END"
529
" FROM v_reports"
530
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
531
" GROUP BY 2 ORDER BY cast(strftime('%%w', mtime) AS INT);"
532
, zUserName
533
);
534
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
535
@ <center><svg width=700 height=400>
536
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
537
@ </svg></centre><hr>
538
}
539
style_table_sorter();
540
@ <table class='statistics-report-table-events sortable' border='0' \
541
@ cellpadding='2' cellspacing='0' data-column-types='ntnx' data-init-sort='1'>
542
@ <thead><tr>
543
@ <th>DoW</th>
544
@ <th>Day</th>
545
@ <th>Events</th>
546
@ <th width='90%%'><!-- relative commits graph --></th>
547
@ </tr></thead><tbody>
548
while( SQLITE_ROW == db_step(&query) ){
549
const int nCount = db_column_int(&query, 1);
550
if(nCount>nMaxEvents){
551
nMaxEvents = nCount;
552
}
553
}
554
db_reset(&query);
555
while( SQLITE_ROW == db_step(&query) ){
556
const int dayNum =db_column_int(&query, 0);
557
const int nCount = db_column_int(&query, 1);
558
int nSize = nCount
559
? (int)(100 * nCount / nMaxEvents)
560
: 0;
561
if(!nCount) continue /* arguable! Possible? */;
562
else if(!nSize) nSize = 1;
563
rowClass = ++nRowNumber % 2;
564
@<tr class='row%d(rowClass)'>
565
@ <td>%d(dayNum)</td>
566
@ <td>%s(daysOfWeek[dayNum])</td>
567
@ <td>%d(nCount)</td>
568
@ <td>
569
@ <div class='statistics-report-graph-line'
570
@ style='width:%d(nSize)%%;'>&nbsp;</div>
571
@ </td>
572
@</tr>
573
}
574
@ </tbody></table>
575
db_finalize(&query);
576
}
577
578
/*
579
** Implements the "byhour" view for /reports. If zUserName is not NULL
580
** then the report is restricted to events created by the named user
581
** account.
582
*/
583
static void stats_report_hour_of_day(const char *zUserName){
584
Stmt query = empty_Stmt;
585
int nRowNumber = 0; /* current TR number */
586
int rowClass = 0; /* counter for alternating
587
row colors */
588
int nMaxEvents = 1; /* max number of events for
589
all rows. */
590
591
stats_report_init_view();
592
db_prepare(&query,
593
"SELECT cast(strftime('%%H', mtime) AS INTEGER) hod,"
594
" COUNT(*) AS eventCount"
595
" FROM v_reports"
596
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
597
" GROUP BY hod ORDER BY hod", zUserName);
598
@ <h1>Timeline Events (%h(stats_report_label_for_type())) by Hour of Day
599
if( zUserName ){
600
@ for user %h(zUserName)
601
}
602
@ </h1>
603
db_multi_exec(
604
"CREATE TEMP VIEW piechart(amt,label) AS"
605
" SELECT count(*), strftime('%%H', mtime) hod"
606
" FROM v_reports"
607
" WHERE ifnull(coalesce(euser,user,'')=%Q,1)"
608
" GROUP BY 2 ORDER BY hod;",
609
zUserName
610
);
611
if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){
612
@ <center><svg width=700 height=400>
613
piechart_render(700, 400, PIE_OTHER|PIE_PERCENT);
614
@ </svg></centre><hr>
615
}
616
style_table_sorter();
617
@ <table class='statistics-report-table-events sortable' border='0' \
618
@ cellpadding='2' cellspacing='0' data-column-types='nnx' data-init-sort='1'>
619
@ <thead><tr>
620
@ <th>Hour</th>
621
@ <th>Events</th>
622
@ <th width='90%%'><!-- relative commits graph --></th>
623
@ </tr></thead><tbody>
624
while( SQLITE_ROW == db_step(&query) ){
625
const int nCount = db_column_int(&query, 1);
626
if(nCount>nMaxEvents){
627
nMaxEvents = nCount;
628
}
629
}
630
db_reset(&query);
631
while( SQLITE_ROW == db_step(&query) ){
632
const int hourNum =db_column_int(&query, 0);
633
const int nCount = db_column_int(&query, 1);
634
int nSize = nCount
635
? (int)(100 * nCount / nMaxEvents)
636
: 0;
637
if(!nCount) continue /* arguable! Possible? */;
638
else if(!nSize) nSize = 1;
639
rowClass = ++nRowNumber % 2;
640
@<tr class='row%d(rowClass)'>
641
@ <td>%d(hourNum)</td>
642
@ <td>%d(nCount)</td>
643
@ <td>
644
@ <div class='statistics-report-graph-line'
645
@ style='width:%d(nSize)%%;'>&nbsp;</div>
646
@ </td>
647
@</tr>
648
}
649
@ </tbody></table>
650
db_finalize(&query);
651
}
652
653
654
/*
655
** Helper for stats_report_by_month_year(), which generates a list of
656
** week numbers. The "y" query parameter is the year in format YYYY.
657
** If zUserName is not NULL then the report is restricted to events
658
** created by the named user account.
659
*/
660
static void stats_report_year_weeks(const char *zUserName){
661
const char *zYear = P("y"); /* Year for which report shown */
662
Stmt q;
663
int nMaxEvents = 1; /* max number of events for
664
all rows. */
665
int iterations = 0; /* # of active time periods. */
666
int rowCount = 0;
667
int total = 0;
668
char *zCurrentWeek; /* Current week number */
669
double rNowFraction = 0.0; /* Fraction of current week that has
670
** passed */
671
672
stats_report_init_view();
673
style_submenu_sql("y", "Year:",
674
"WITH RECURSIVE a(b) AS ("
675
" SELECT substr(date('now'),1,4) UNION ALL"
676
" SELECT b-1 FROM a"
677
" WHERE b>0+(SELECT substr(date(min(mtime)),1,4) FROM event)"
678
") SELECT b, b FROM a ORDER BY b DESC");
679
if( zYear==0 || strlen(zYear)!=4 ){
680
zYear = db_text("1970","SELECT substr(date('now'),1,4);");
681
}
682
cgi_printf("<br>\n");
683
db_prepare(&q,
684
"SELECT DISTINCT strftime('%%W',mtime) AS wk, "
685
" count(*) AS n "
686
" FROM v_reports "
687
" WHERE %Q=substr(date(mtime),1,4) "
688
" AND mtime < current_timestamp "
689
" AND ifnull(coalesce(euser,user,'')=%Q,1)"
690
" GROUP BY wk ORDER BY wk DESC", zYear, zUserName);
691
@ <h1>Timeline events (%h(stats_report_label_for_type()))
692
@ for the calendar weeks of %h(zYear)
693
if( zUserName ){
694
@ for user %h(zUserName)
695
}
696
@ </h1>
697
zCurrentWeek = db_text(0,
698
"SELECT strftime('%%W','now') WHERE date() LIKE '%q%%'",
699
zYear);
700
if( zCurrentWeek ){
701
rNowFraction = db_double(0.5,
702
"SELECT (unixepoch()-unixepoch('now','weekday 0','-7 days'))/604800.0;");
703
}
704
style_table_sorter();
705
cgi_printf("<table class='statistics-report-table-events sortable' "
706
"border='0' cellpadding='2' width='100%%' "
707
"cellspacing='0' data-column-types='tnx' data-init-sort='0'>\n");
708
cgi_printf("<thead><tr>"
709
"<th>Week</th>"
710
"<th>Events</th>"
711
"<th width='90%%'><!-- relative commits graph --></th>"
712
"</tr></thead>\n"
713
"<tbody>\n");
714
while( SQLITE_ROW == db_step(&q) ){
715
int nCount = db_column_int(&q, 1);
716
if( zCurrentWeek!=0
717
&& strcmp(db_column_text(&q,0),zCurrentWeek)==0
718
&& rNowFraction>0.05
719
){
720
nCount = (int)(((double)nCount)/rNowFraction);
721
}
722
if(nCount>nMaxEvents){
723
nMaxEvents = nCount;
724
}
725
++iterations;
726
}
727
db_reset(&q);
728
while( SQLITE_ROW == db_step(&q) ){
729
const char *zWeek = db_column_text(&q,0);
730
const int nCount = db_column_int(&q,1);
731
int nSize = (nCount>0 && nMaxEvents>0)
732
? (int)(100 * nCount / nMaxEvents)
733
: 0;
734
if(!nSize) nSize = 1;
735
total += nCount;
736
cgi_printf("<tr class='row%d'>", ++rowCount % 2 );
737
cgi_printf("<td><a href='%R/timeline?yw=%t%s&y=%s",
738
zYear, zWeek,
739
statsReportTimelineYFlag);
740
if( zUserName ){
741
cgi_printf("&u=%t",zUserName);
742
}
743
cgi_printf("'>%s</a></td>",zWeek);
744
745
cgi_printf("<td>%d</td>",nCount);
746
cgi_printf("<td style='white-space: nowrap;'>");
747
if( nCount ){
748
if( zCurrentWeek!=0
749
&& strcmp(zWeek, zCurrentWeek)==0
750
&& rNowFraction>0.05
751
&& nMaxEvents>0
752
){
753
/* If the timespan covered by this row contains "now", then project
754
** the number of changes until the completion of the week and
755
** show a dashed box of that projection. */
756
int nProj = (int)(((double)nCount)/rNowFraction);
757
int nExtra = (int)(((double)nCount)/rNowFraction) - nCount;
758
int nXSize = (100 * nExtra)/nMaxEvents;
759
@ <span class='statistics-report-graph-line' \
760
@ style='display:inline-block;min-width:%d(nSize)%%;'>&nbsp;</span>\
761
@ <span class='statistics-report-graph-extra' title='%d(nProj)' \
762
@ style='display:inline-block;min-width:%d(nXSize)%%;'>&nbsp;</span>\
763
}else{
764
@ <div class='statistics-report-graph-line' \
765
@ style='width:%d(nSize)%%;'>&nbsp;</div> \
766
}
767
}
768
cgi_printf("</td></tr>\n");
769
}
770
db_finalize(&q);
771
cgi_printf("</tbody></table>");
772
if(total){
773
int nAvg = iterations ? (total/iterations) : 0;
774
cgi_printf("<br><div>Total events: %d<br>"
775
"Average per active week: %d</div>",
776
total, nAvg);
777
}
778
}
779
780
781
/*
782
** Generate a report that shows the most recent change for each user.
783
*/
784
static void stats_report_last_change(void){
785
Stmt s;
786
double rNow;
787
char *zBaseUrl;
788
789
stats_report_init_view();
790
style_table_sorter();
791
@ <h1>Event Summary
792
@ (%s(stats_report_label_for_type())) by User</h1>
793
@ <table border=1 class='statistics-report-table-events sortable' \
794
@ cellpadding=2 cellspacing=0 data-column-types='tNK' data-init-sort='3'>
795
@ <thead><tr>
796
@ <th>User<th>Total Changes<th>Last Change</tr></thead>
797
@ <tbody>
798
zBaseUrl = mprintf("%R/timeline?y=%t&u=", PD("type","ci"));
799
db_prepare(&s,
800
"SELECT coalesce(euser,user),"
801
" count(*),"
802
" max(mtime)"
803
" FROM v_reports"
804
" GROUP BY 1"
805
" ORDER BY 3 DESC"
806
);
807
rNow = db_double(0.0, "SELECT julianday('now');");
808
while( db_step(&s)==SQLITE_ROW ){
809
const char *zUser = db_column_text(&s, 0);
810
int cnt = db_column_int(&s, 1);
811
double rMTime = db_column_double(&s,2);
812
char *zAge = human_readable_age(rNow - rMTime);
813
@ <tr>
814
@ <td><a href='%s(zBaseUrl)%t(zUser)'>%h(zUser)</a>
815
@ <td>%d(cnt)
816
@ <td data-sortkey='%f(rMTime)' style='white-space:nowrap'>%s(zAge?zAge:"")
817
@ </tr>
818
fossil_free(zAge);
819
}
820
@ </tbody></table>
821
db_finalize(&s);
822
}
823
824
825
/* Report types
826
*/
827
#define RPT_BYFILE 1
828
#define RPT_BYMONTH 2
829
#define RPT_BYUSER 3
830
#define RPT_BYWEEK 4
831
#define RPT_BYWEEKDAY 5
832
#define RPT_BYYEAR 6
833
#define RPT_LASTCHNG 7 /* Last change made for each user */
834
#define RPT_BYHOUR 8 /* hour-of-day */
835
#define RPT_NONE 0 /* None of the above */
836
837
/*
838
** WEBPAGE: reports
839
**
840
** Shows activity reports for the repository.
841
**
842
** Query Parameters:
843
**
844
** view=REPORT_NAME Valid REPORT_NAME values:
845
** * byyear
846
** * bymonth
847
** * byweek
848
** * byweekday
849
** * byhour
850
** * byuser
851
** * byfile
852
** * lastchng
853
** user=NAME Restricts statistics to the given user
854
** type=TYPE Restricts the report to a specific event type:
855
** * all (everything),
856
** * ci (check-in)
857
** * m (merge check-in),
858
** * n (non-merge check-in)
859
** * f (forum post)
860
** * w (wiki page change)
861
** * t (ticket change)
862
** * g (tag added or removed)
863
** Defaulting to all event types.
864
** from=DATETIME Consider only events after this timestamp (requires to)
865
** to=DATETIME Consider only events before this timestamp (requires from)
866
**
867
**
868
** The view-specific query parameters include:
869
**
870
** view=byweek:
871
**
872
** y=YYYY The year to report (default is the server's
873
** current year).
874
*/
875
void stats_report_page(){
876
const char *zView = P("view"); /* Which view/report to show. */
877
int eType = RPT_NONE; /* Numeric code for view/report to show */
878
int i; /* Loop counter */
879
const char *zUserName; /* Name of user */
880
const char *azView[16]; /* Drop-down menu of view types */
881
static const struct {
882
const char *zName; /* Name of view= screen type */
883
const char *zVal; /* Value of view= query parameter */
884
int eType; /* Corresponding RPT_* define */
885
} aViewType[] = {
886
{ "File Changes","byfile", RPT_BYFILE },
887
{ "Last Change", "lastchng", RPT_LASTCHNG },
888
{ "By Month", "bymonth", RPT_BYMONTH },
889
{ "By User", "byuser", RPT_BYUSER },
890
{ "By Week", "byweek", RPT_BYWEEK },
891
{ "By Weekday", "byweekday", RPT_BYWEEKDAY },
892
{ "By Year", "byyear", RPT_BYYEAR },
893
{ "By Hour", "byhour", RPT_BYHOUR },
894
};
895
static const char *const azType[] = {
896
"a", "All Changes",
897
"ci", "Check-ins",
898
"f", "Forum Posts",
899
"m", "Merge check-ins",
900
"n", "Non-merge check-ins",
901
"g", "Tags",
902
"e", "Tech Notes",
903
"t", "Tickets",
904
"w", "Wiki"
905
};
906
907
login_check_credentials();
908
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
909
zUserName = P("user");
910
if( zUserName==0 ) zUserName = P("u");
911
if( zUserName && zUserName[0]==0 ) zUserName = 0;
912
if( zView==0 ){
913
zView = "byuser";
914
cgi_replace_query_parameter("view","byuser");
915
}
916
for(i=0; i<count(aViewType); i++){
917
if( fossil_strcmp(zView, aViewType[i].zVal)==0 ){
918
eType = aViewType[i].eType;
919
break;
920
}
921
}
922
cgi_check_for_malice();
923
if( eType!=RPT_NONE ){
924
int nView = 0; /* Slots used in azView[] */
925
for(i=0; i<count(aViewType); i++){
926
azView[nView++] = aViewType[i].zVal;
927
azView[nView++] = aViewType[i].zName;
928
}
929
if( eType!=RPT_BYFILE ){
930
style_submenu_multichoice("type", count(azType)/2, azType, 0);
931
}
932
style_submenu_multichoice("view", nView/2, azView, 0);
933
if( eType!=RPT_BYUSER && eType!=RPT_LASTCHNG ){
934
style_submenu_sql("user","User:",
935
"SELECT '', 'All Users' UNION ALL "
936
"SELECT x, x FROM ("
937
" SELECT DISTINCT trim(coalesce(euser,user)) AS x FROM event %s"
938
" ORDER BY 1 COLLATE nocase) WHERE x!=''",
939
eType==RPT_BYFILE ? "WHERE type='ci'" : ""
940
);
941
}
942
}
943
style_submenu_element("Stats", "%R/stat");
944
style_header("Activity Reports");
945
switch( eType ){
946
case RPT_BYYEAR:
947
stats_report_by_month_year(0, zUserName);
948
break;
949
case RPT_BYMONTH:
950
stats_report_by_month_year(1, zUserName);
951
break;
952
case RPT_BYWEEK:
953
stats_report_year_weeks(zUserName);
954
break;
955
default:
956
case RPT_BYUSER:
957
stats_report_by_user();
958
break;
959
case RPT_BYWEEKDAY:
960
stats_report_day_of_week(zUserName);
961
break;
962
case RPT_BYFILE:
963
stats_report_by_file(zUserName);
964
break;
965
case RPT_BYHOUR:
966
stats_report_hour_of_day(zUserName);
967
break;
968
case RPT_LASTCHNG:
969
stats_report_last_change();
970
break;
971
}
972
style_finish_page();
973
}
974

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button