Fossil SCM

fossil-scm / src / timeline.c
Blame History Raw 4193 lines
1
/*
2
** Copyright (c) 2007 D. Richard Hipp
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 timeline web page
19
**
20
*/
21
#include "config.h"
22
#include <string.h>
23
#include <time.h>
24
#include "timeline.h"
25
26
/*
27
** The value of one second in Julian day notation
28
*/
29
#define ONE_SECOND (1.0/86400.0)
30
31
/*
32
** timeline mode options
33
*/
34
#define TIMELINE_MODE_NONE 0
35
#define TIMELINE_MODE_BEFORE 1
36
#define TIMELINE_MODE_AFTER 2
37
#define TIMELINE_MODE_CHILDREN 3
38
#define TIMELINE_MODE_PARENTS 4
39
40
#define TIMELINE_FMT_ONELINE \
41
"%h %c"
42
#define TIMELINE_FMT_MEDIUM \
43
"Commit: %h%nDate: %d%nAuthor: %a%nComment: %c"
44
#define TIMELINE_FMT_FULL \
45
"Commit: %H%nDate: %d%nAuthor: %a%nComment: %c%n"\
46
"Branch: %b%nTags: %t%nPhase: %p"
47
/*
48
** Add an appropriate tag to the output if "rid" is unpublished (private)
49
*/
50
#define UNPUB_TAG "<em>(unpublished)</em>"
51
void tag_private_status(int rid){
52
if( content_is_private(rid) ){
53
cgi_printf(" %s", UNPUB_TAG);
54
}
55
}
56
57
/*
58
** Generate a hyperlink to a version.
59
*/
60
void hyperlink_to_version(const char *zVerHash){
61
if( g.perm.Hyperlink ){
62
@ %z(chref("timelineHistLink","%R/info/%!S",zVerHash))[%S(zVerHash)]</a>
63
}else{
64
@ <span class="timelineHistDsp">[%S(zVerHash)]</span>
65
}
66
}
67
68
/*
69
** Generate a hyperlink to a date & time.
70
*/
71
void hyperlink_to_date(const char *zDate, const char *zSuffix){
72
if( zSuffix==0 ) zSuffix = "";
73
if( g.perm.Hyperlink ){
74
@ %z(href("%R/timeline?c=%T",zDate))%s(zDate)</a>%s(zSuffix)
75
}else{
76
@ %s(zDate)%s(zSuffix)
77
}
78
}
79
80
/*
81
** Generate a hyperlink to a user. This will link to a timeline showing
82
** events by that user. If the date+time is specified, then the timeline
83
** is centered on that date+time.
84
*/
85
void hyperlink_to_user(const char *zU, const char *zD, const char *zSuf){
86
if( zU==0 || zU[0]==0 ) zU = "anonymous";
87
if( zSuf==0 ) zSuf = "";
88
if( g.perm.Hyperlink ){
89
if( zD && zD[0] ){
90
@ %z(href("%R/timeline?c=%T&u=%T&y=a",zD,zU))%h(zU)</a>%s(zSuf)
91
}else{
92
@ %z(href("%R/timeline?u=%T&y=a",zU))%h(zU)</a>%s(zSuf)
93
}
94
}else{
95
@ %s(zU)
96
}
97
}
98
99
/*
100
** Allowed flags for the tmFlags argument to www_print_timeline
101
*/
102
#if INTERFACE
103
#define TIMELINE_ARTID 0x0000001 /* Show artifact IDs on non-check-in lines*/
104
#define TIMELINE_LEAFONLY 0x0000002 /* Show "Leaf" but not "Merge", "Fork" etc*/
105
#define TIMELINE_BRIEF 0x0000004 /* Combine adjacent elements of same obj */
106
#define TIMELINE_GRAPH 0x0000008 /* Compute a graph */
107
#define TIMELINE_DISJOINT 0x0000010 /* Elements are not contiguous */
108
#define TIMELINE_FCHANGES 0x0000020 /* Detail file changes */
109
#define TIMELINE_BRCOLOR 0x0000040 /* Background color by branch name */
110
#define TIMELINE_UCOLOR 0x0000080 /* Background color by user */
111
#define TIMELINE_FRENAMES 0x0000100 /* Detail only file name changes */
112
#define TIMELINE_UNHIDE 0x0000200 /* Unhide check-ins with "hidden" tag */
113
#define TIMELINE_SHOWRID 0x0000400 /* Show RID values in addition to hashes */
114
#define TIMELINE_BISECT 0x0000800 /* Show supplemental bisect information */
115
#define TIMELINE_COMPACT 0x0001000 /* Use the "compact" view style */
116
#define TIMELINE_VERBOSE 0x0002000 /* Use the "detailed" view style */
117
#define TIMELINE_MODERN 0x0004000 /* Use the "modern" view style */
118
#define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */
119
#define TIMELINE_CLASSIC 0x0010000 /* Use the "classic" view style */
120
#define TIMELINE_SIMPLE 0x0020000 /* Use the "simple" view style */
121
#define TIMELINE_INLINE 0x0033000 /* Mask for views with in-line display */
122
#define TIMELINE_VIEWS 0x003f000 /* Mask for all of the view styles */
123
#define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */
124
#define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */
125
#define TIMELINE_CHPICK 0x0400000 /* Show cherrypick merges */
126
#define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */
127
#define TIMELINE_XMERGE 0x1000000 /* Omit merges from off-graph nodes */
128
#define TIMELINE_NOTKT 0x2000000 /* Omit extra ticket classes */
129
#define TIMELINE_FORUMTXT 0x4000000 /* Render all forum messages */
130
#define TIMELINE_REFS 0x8000000 /* Output intended for References tab */
131
#define TIMELINE_DELTA 0x10000000 /* Background color shows delta manifests */
132
#define TIMELINE_NOCOLOR 0x20000000 /* No colors except for highlights */
133
#endif
134
135
/*
136
** Return a new timelineTable id.
137
*/
138
int timeline_tableid(void){
139
static int id = 0;
140
return id++;
141
}
142
143
/*
144
** Return true if the checking identified by "rid" has a valid "closed"
145
** tag.
146
*/
147
static int has_closed_tag(int rid){
148
static Stmt q;
149
int res = 0;
150
db_static_prepare(&q,
151
"SELECT 1 FROM tagxref WHERE rid=$rid AND tagid=%d AND tagtype>0",
152
TAG_CLOSED);
153
db_bind_int(&q, "$rid", rid);
154
res = db_step(&q)==SQLITE_ROW;
155
db_reset(&q);
156
return res;
157
}
158
159
/*
160
** Return the text of the unformatted
161
** forum post given by the RID in the argument.
162
*/
163
static void forum_post_content_function(
164
sqlite3_context *context,
165
int argc,
166
sqlite3_value **argv
167
){
168
int rid = sqlite3_value_int(argv[0]);
169
Manifest *pPost = manifest_get(rid, CFTYPE_FORUM, 0);
170
if( pPost ){
171
sqlite3_result_text(context, pPost->zWiki, -1, SQLITE_TRANSIENT);
172
manifest_destroy(pPost);
173
}
174
}
175
176
177
/*
178
** This routine generates the default "extra" text after the description
179
** in a timeline.
180
**
181
** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
182
**
183
** This routine is used if no xExtra argument is supplied to
184
** www_print_timeline().
185
*/
186
void timeline_extra(
187
Stmt *pQuery, /* Current row of the timeline query */
188
int tmFlags, /* Flags to www_print_timeline() */
189
const char *zThisUser, /* Suppress links to this user */
190
const char *zThisTag /* Suppress links to this tag */
191
){
192
int rid = db_column_int(pQuery, 0);
193
const char *zUuid = db_column_text(pQuery, 1);
194
const char *zDate = db_column_text(pQuery, 2);
195
const char *zType = db_column_text(pQuery, 7);
196
const char *zUser = db_column_text(pQuery, 4);
197
const char *zTagList = db_column_text(pQuery, 8);
198
int tagid = db_column_int(pQuery, 9);
199
const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
200
201
if( (tmFlags & TIMELINE_INLINE)!=0 ){
202
cgi_printf("(");
203
}
204
205
if( (tmFlags & TIMELINE_CLASSIC)==0 ){
206
if( zType[0]=='c' ){
207
const char *zPrefix = 0;
208
static int markLeaves = -1;
209
if( markLeaves<0 ){
210
markLeaves = db_get_int("timeline-mark-leaves",1);
211
if( markLeaves<0 ) markLeaves = 1;
212
}
213
if( strcmp(zUuid, MANIFEST_UUID)==0 ){
214
/* This will only ever happen when Fossil is drawing a timeline for
215
** its own self-host repository. If the timeline shows the specific
216
** check-in corresponding to the current executable, then tag that
217
** check-in with "self" */
218
zPrefix = "self&nbsp;";
219
}else if( markLeaves && db_column_int(pQuery,5) ){
220
if( markLeaves==1 ){
221
zPrefix = has_closed_tag(rid) ? "closed&nbsp;" : "leaf&nbsp;";
222
}else{
223
zPrefix = has_closed_tag(rid) ?
224
"<span class='timelineLeaf'>Closed-Leaf</span>\n" :
225
"<span class='timelineLeaf'>Leaf</span>\n";
226
}
227
}
228
cgi_printf("%scheck-in:&nbsp;%z<span class='timelineHash'>"
229
"%S</span></a> ",
230
zPrefix, href("%R/info/%!S",zUuid),zUuid);
231
}else if( zType[0]=='e' && tagid ){
232
cgi_printf("technote:&nbsp;");
233
hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
234
}else{
235
cgi_printf("artifact:&nbsp;%z%S</a> ",
236
href("%R/info/%!S",zUuid),zUuid);
237
}
238
}else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t'
239
|| zType[0]=='n' || zType[0]=='f'){
240
cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
241
}
242
243
if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
244
@ <span class='timelineEllipsis' id='ellipsis-%d(rid)' \
245
@ data-id='%d(rid)'>...</span>
246
@ <span class='clutter' id='detail-%d(rid)'>
247
}
248
249
if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
250
char *zLink;
251
if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){
252
zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate);
253
}else{
254
zLink = mprintf("%R/timeline?u=%h&c=%t&y=a&vfx", zDispUser, zDate);
255
}
256
cgi_printf("user:&nbsp;%z%h</a>", href("%z",zLink), zDispUser);
257
}else{
258
cgi_printf("user:&nbsp;%h", zDispUser);
259
}
260
261
/* Generate the "tags: TAGLIST" at the end of the comment, together
262
** with hyperlinks to the tag list.
263
*/
264
if( zTagList && zTagList[0]==0 ) zTagList = 0;
265
if( zTagList ){
266
if( g.perm.Hyperlink ){
267
int i;
268
const char *z = zTagList;
269
Blob links;
270
blob_zero(&links);
271
while( z && z[0] ){
272
for(i=0; z[i] && (z[i]!=',' || z[i+1]!=' '); i++){}
273
if( zThisTag==0 || memcmp(z, zThisTag, i)!=0 || zThisTag[i]!=0 ){
274
blob_appendf(&links,
275
"%z%#h</a>%.2s",
276
href("%R/timeline?r=%#t&c=%t",i,z,zDate), i,z, &z[i]
277
);
278
}else{
279
blob_appendf(&links, "%#h", i+2, z);
280
}
281
if( z[i]==0 ) break;
282
z += i+2;
283
}
284
cgi_printf(" tags:&nbsp;%s", blob_str(&links));
285
blob_reset(&links);
286
}else{
287
cgi_printf(" tags:&nbsp;%h", zTagList);
288
}
289
}
290
291
if( tmFlags & TIMELINE_SHOWRID ){
292
int srcId = delta_source_rid(rid);
293
if( srcId ){
294
cgi_printf(" id:&nbsp;%z%d&larr;%d</a>",
295
href("%R/deltachain/%d",rid), rid, srcId);
296
}else{
297
cgi_printf(" id:&nbsp;%z%d</a>",
298
href("%R/deltachain/%d",rid), rid);
299
}
300
}
301
tag_private_status(rid);
302
303
if( (tmFlags & TIMELINE_SIMPLE)!=0 ){
304
cgi_printf("</span>"); /* End of the declutter span */
305
}
306
307
/* End timelineDetail */
308
if( (tmFlags & TIMELINE_INLINE)!=0 ){
309
cgi_printf(")");
310
}
311
}
312
313
314
/*
315
** SETTING: timeline-truncate-at-blank boolean default=off
316
**
317
** If enabled, check-in comments displayed on the timeline are truncated
318
** at the first blank line of the comment text. The comment text after
319
** the first blank line is only seen in the /info or similar pages that
320
** show details about the check-in.
321
*/
322
/*
323
** SETTING: timeline-tslink-info boolean default=off
324
**
325
** The hyperlink on the timestamp associated with each timeline entry,
326
** on the far left-hand side of the screen, normally targets another
327
** /timeline page that shows the entry in context. However, if this
328
** option is turned on, that hyperlink targets the /info page showing
329
** the details of the entry.
330
*/
331
/*
332
** SETTING: timeline-mark-leaves width=5 default=1
333
**
334
** Determine whether or not leaf check-ins are marked as such in the
335
** details section of the timeline. The value is an integer between 0
336
** and 2:
337
**
338
** 0 Do not show any special marking for leaf check-ins.
339
**
340
** 1 Show just "leaf" or "closed"
341
**
342
** 2 Show "Leaf" or "Closed-Leaf" with emphasis
343
**
344
** The default is currently 1. Prior to 2025-10-19, the default was 2.
345
** This setting has no effect on the "Classic" view, which always behaves
346
** as if the setting were 2.
347
*/
348
349
/*
350
** Output a timeline in the web format given a query. The query
351
** should return these columns:
352
**
353
** 0. rid
354
** 1. artifact hash
355
** 2. Date/Time
356
** 3. Comment string
357
** 4. User
358
** 5. True if is a leaf
359
** 6. background color
360
** 7. type ("ci", "w", "t", "e", "g", "f", "div")
361
** 8. list of symbolic tags.
362
** 9. tagid for ticket or wiki or event
363
** 10. Short comment to user for repeated tickets and wiki
364
*/
365
void www_print_timeline(
366
Stmt *pQuery, /* Query to implement the timeline */
367
int tmFlags, /* Flags controlling display behavior */
368
const char *zThisUser, /* Suppress links to this user */
369
const char *zThisTag, /* Suppress links to this tag */
370
Matcher *pLeftBranch, /* Comparison function to use for zLeftBranch */
371
int selectedRid, /* Highlight the line with this RID value or zero */
372
int secondRid, /* Secondary highlight (or zero) */
373
void (*xExtra)(Stmt*,int,const char*,const char*) /* generate "extra" text */
374
){
375
int mxWikiLen;
376
Blob comment;
377
int prevTagid = 0;
378
int suppressCnt = 0;
379
char zPrevDate[20];
380
GraphContext *pGraph = 0;
381
int prevWasDivider = 0; /* True if previous output row was <hr> */
382
int fchngQueryInit = 0; /* True if fchngQuery is initialized */
383
Stmt fchngQuery; /* Query for file changes on check-ins */
384
int pendingEndTr = 0; /* True if a </td></tr> is needed */
385
int vid = 0; /* Current check-out version */
386
int dateFormat = 0; /* 0: HH:MM (default) */
387
int bCommentGitStyle = 0; /* Only show comments through first blank line */
388
const char *zStyle; /* Sub-name for classes for the style */
389
const char *zDateFmt;
390
int iTableId = timeline_tableid();
391
int bTimestampLinksToInfo; /* True if timestamp hyperlinks go to the /info
392
** page rather than the /timeline page */
393
const char *zMainBranch = db_main_branch();
394
395
396
if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
397
vid = db_lget_int("checkout", 0);
398
}
399
if( xExtra==0 ) xExtra = timeline_extra;
400
zPrevDate[0] = 0;
401
mxWikiLen = db_get_int("timeline-max-comment", 0);
402
dateFormat = db_get_int("timeline-date-format", 0);
403
bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
404
bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
405
if( (tmFlags & TIMELINE_VIEWS)==0 ){
406
tmFlags |= timeline_ss_cookie();
407
}
408
if( tmFlags & TIMELINE_COLUMNAR ){
409
zStyle = "Columnar";
410
}else if( tmFlags & TIMELINE_COMPACT ){
411
zStyle = "Compact";
412
}else if( tmFlags & TIMELINE_SIMPLE ){
413
zStyle = "Simple";
414
}else if( tmFlags & TIMELINE_VERBOSE ){
415
zStyle = "Verbose";
416
}else if( tmFlags & TIMELINE_CLASSIC ){
417
zStyle = "Classic";
418
}else{
419
zStyle = "Modern";
420
}
421
zDateFmt = P("datefmt");
422
if( zDateFmt ) dateFormat = atoi(zDateFmt);
423
pGraph = graph_init();
424
if( (tmFlags & TIMELINE_CHPICK)!=0
425
&& !db_table_exists("repository","cherrypick")
426
){
427
tmFlags &= ~TIMELINE_CHPICK;
428
}
429
@ <table id="timelineTable%d(iTableId)" class="timelineTable"> \
430
@ <!-- tmFlags: 0x%x(tmFlags) -->
431
blob_zero(&comment);
432
while( db_step(pQuery)==SQLITE_ROW ){
433
int rid = db_column_int(pQuery, 0);
434
const char *zUuid = db_column_text(pQuery, 1);
435
int isLeaf = db_column_int(pQuery, 5);
436
const char *zBgClr = db_column_text(pQuery, 6);
437
const char *zDate = db_column_text(pQuery, 2);
438
const char *zType = db_column_text(pQuery, 7);
439
const char *zUser = db_column_text(pQuery, 4);
440
int tagid = db_column_int(pQuery, 9);
441
char *zBr = 0; /* Branch */
442
int commentColumn = 3; /* Column containing comment text */
443
int modPending; /* Pending moderation */
444
char *zDateLink; /* URL for the link on the timestamp */
445
int drawDetailEllipsis; /* True to show ellipsis in place of detail */
446
int gidx = 0; /* Graph row identifier */
447
int isSelectedOrCurrent = 0; /* True if current row is selected */
448
const char *zExtraClass = "";
449
char zTime[20];
450
451
if( zDate==0 ){
452
zDate = "YYYY-MM-DD HH:MM:SS"; /* Something wrong with the repo */
453
}
454
modPending = moderation_pending(rid);
455
if( tagid ){
456
if( modPending ) tagid = -tagid;
457
if( tagid==prevTagid ){
458
if( tmFlags & TIMELINE_BRIEF ){
459
suppressCnt++;
460
continue;
461
}else{
462
commentColumn = 10;
463
}
464
}
465
}
466
prevTagid = tagid;
467
if( suppressCnt ){
468
@ <span class="timelineDisabled">... %d(suppressCnt) similar
469
@ event%s(suppressCnt>1?"s":"") omitted.</span>
470
suppressCnt = 0;
471
}
472
if( pendingEndTr ){
473
@ </td></tr>
474
pendingEndTr = 0;
475
}
476
if( fossil_strcmp(zType,"div")==0 ){
477
if( !prevWasDivider ){
478
@ <tr><td colspan="3"><hr class="timelineMarker"></td></tr>
479
}
480
prevWasDivider = 1;
481
continue;
482
}
483
prevWasDivider = 0;
484
/* Date format codes:
485
** (0) HH:MM
486
** (1) HH:MM:SS
487
** (2) YYYY-MM-DD HH:MM
488
** (3) YYMMDD HH:MM
489
** (4) (off)
490
*/
491
if( dateFormat<2 ){
492
if( fossil_strnicmp(zDate, zPrevDate, 10) ){
493
sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
494
@ <tr class="timelineDateRow"><td>
495
@ <div class="divider timelineDate">%s(zPrevDate)</div>
496
@ </td><td></td><td></td></tr>
497
}
498
memcpy(zTime, &zDate[11], 5+dateFormat*3);
499
zTime[5+dateFormat*3] = 0;
500
}else if( 2==dateFormat ){
501
/* YYYY-MM-DD HH:MM */
502
sqlite3_snprintf(sizeof(zTime), zTime, "%.16s", zDate);
503
}else if( 3==dateFormat ){
504
/* YYMMDD HH:MM */
505
int pos = 0;
506
zTime[pos++] = zDate[2]; zTime[pos++] = zDate[3]; /* YY */
507
zTime[pos++] = zDate[5]; zTime[pos++] = zDate[6]; /* MM */
508
zTime[pos++] = zDate[8]; zTime[pos++] = zDate[9]; /* DD */
509
zTime[pos++] = ' ';
510
zTime[pos++] = zDate[11]; zTime[pos++] = zDate[12]; /* HH */
511
zTime[pos++] = ':';
512
zTime[pos++] = zDate[14]; zTime[pos++] = zDate[15]; /* MM */
513
zTime[pos++] = 0;
514
}else{
515
zTime[0] = 0;
516
}
517
pendingEndTr = 1;
518
if( rid==selectedRid ){
519
@ <tr class="timelineSelected">
520
isSelectedOrCurrent = 1;
521
}else if( rid==secondRid ){
522
@ <tr class="timelineSelected timelineSecondary">
523
isSelectedOrCurrent = 1;
524
}else if( rid==vid ){
525
@ <tr class="timelineCurrent">
526
isSelectedOrCurrent = 1;
527
}else {
528
@ <tr>
529
}
530
if( zType[0]=='t' && tagid && (tmFlags & TIMELINE_NOTKT)==0 ){
531
char *zTktid = db_text(0, "SELECT substr(tagname,5) FROM tag"
532
" WHERE tagid=%d", tagid);
533
if( zTktid ){
534
int isClosed = 0;
535
if( is_ticket(zTktid, &isClosed) && isClosed ){
536
zExtraClass = " tktTlClosed";
537
}else{
538
zExtraClass = " tktTlOpen";
539
}
540
fossil_free(zTktid);
541
}
542
}
543
if( zType[0]=='e' && tagid ){
544
if( bTimestampLinksToInfo ){
545
char *zId;
546
zId = db_text(0, "SELECT substr(tagname, 7) FROM tag WHERE tagid=%d",
547
tagid);
548
zDateLink = href("%R/technote/%s",zId);
549
free(zId);
550
}else{
551
zDateLink = href("%R/timeline?c=%t&y=a",zDate);
552
}
553
}else if( zUuid ){
554
if( bTimestampLinksToInfo ){
555
zDateLink = chref("timelineHistLink", "%R/info/%!S", zUuid);
556
}else{
557
zDateLink = chref("timelineHistLink", "%R/timeline?c=%!S&y=a", zUuid);
558
}
559
}else{
560
zDateLink = mprintf("<a>");
561
}
562
@ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td>
563
@ <td class="timelineGraph">
564
if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
565
/* Don't use the requested background color. Use the background color
566
** override from query parameters instead. */
567
if( tmFlags & TIMELINE_UCOLOR ){
568
zBgClr = zUser ? user_color(zUser) : 0;
569
}else if( tmFlags & TIMELINE_NOCOLOR ){
570
zBgClr = 0;
571
}else if( zType[0]=='c' ){
572
static Stmt qdelta;
573
db_static_prepare(&qdelta, "SELECT baseid IS NULL FROM plink"
574
" WHERE cid=:rid");
575
db_bind_int(&qdelta, ":rid", rid);
576
if( db_step(&qdelta)!=SQLITE_ROW ){
577
zBgClr = 0; /* Not a check-in */
578
}else if( db_column_int(&qdelta, 0) ){
579
zBgClr = hash_color("b"); /* baseline manifest */
580
}else{
581
zBgClr = hash_color("f"); /* delta manifest */
582
}
583
db_reset(&qdelta);
584
}
585
}else{
586
/* Make sure the user-specified background color is reasonable */
587
zBgClr = reasonable_bg_color(zBgClr, 0);
588
}
589
if( zType[0]=='c'
590
&& (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
591
){
592
zBr = branch_of_rid(rid);
593
if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){
594
/* If no background color is specified, use a color based on the
595
** branch name */
596
if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
597
}else if( zBr==0 || strcmp(zBr, zMainBranch)==0 ){
598
zBgClr = 0;
599
}else{
600
zBgClr = hash_color(zBr);
601
}
602
}
603
}
604
if( zType[0]=='c' && pGraph ){
605
int nParent = 0;
606
int nCherrypick = 0;
607
GraphRowId aParent[GR_MAX_RAIL];
608
static Stmt qparent;
609
if( tmFlags & TIMELINE_GRAPH ){
610
db_static_prepare(&qparent,
611
"SELECT pid FROM plink"
612
" WHERE cid=:rid AND pid NOT IN phantom"
613
" ORDER BY isprim DESC /*sort*/"
614
);
615
db_bind_int(&qparent, ":rid", rid);
616
while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
617
aParent[nParent++] = db_column_int(&qparent, 0);
618
}
619
db_reset(&qparent);
620
if( (tmFlags & TIMELINE_CHPICK)!=0 && nParent>0 ){
621
static Stmt qcherrypick;
622
db_static_prepare(&qcherrypick,
623
"SELECT parentid FROM cherrypick"
624
" WHERE childid=:rid AND parentid NOT IN phantom"
625
);
626
db_bind_int(&qcherrypick, ":rid", rid);
627
while( db_step(&qcherrypick)==SQLITE_ROW && nParent<count(aParent) ){
628
aParent[nParent++] = db_column_int(&qcherrypick, 0);
629
nCherrypick++;
630
}
631
db_reset(&qcherrypick);
632
}
633
}
634
gidx = graph_add_row(pGraph, rid, nParent, nCherrypick, aParent,
635
zBr, zBgClr, zUuid,
636
isLeaf ? isLeaf + 2 * has_closed_tag(rid) : 0);
637
@ <div id="m%d(gidx)" class="tl-nodemark"></div>
638
}else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
639
/* For technotes, make a graph node with nParent==(-1). This will
640
** not actually draw anything on the graph, but it will set the
641
** background color of the timeline entry */
642
gidx = graph_add_row(pGraph, rid, -1, 0, 0, zBr, zBgClr, zUuid, 0);
643
@ <div id="m%d(gidx)" class="tl-nodemark"></div>
644
}
645
fossil_free(zBr);
646
@</td>
647
if( !isSelectedOrCurrent ){
648
@ <td class="timeline%s(zStyle)Cell%s(zExtraClass)" id='mc%d(gidx)'>
649
}else{
650
@ <td class="timeline%s(zStyle)Cell%s(zExtraClass)">
651
}
652
if( pGraph ){
653
if( zType[0]=='e' ){
654
@ <b>Note:</b>
655
}else if( zType[0]!='c' ){
656
@ &bull;
657
}
658
}
659
if( modPending ){
660
@ <span class="modpending">(Awaiting Moderator Approval)</span>
661
}
662
if( (tmFlags & TIMELINE_BISECT)!=0 && zType[0]=='c' ){
663
static Stmt bisectQuery;
664
db_static_prepare(&bisectQuery,
665
"SELECT seq, stat FROM bilog WHERE rid=:rid AND seq");
666
db_bind_int(&bisectQuery, ":rid", rid);
667
if( db_step(&bisectQuery)==SQLITE_ROW ){
668
@ <b>%s(db_column_text(&bisectQuery,1))</b>
669
@ (%d(db_column_int(&bisectQuery,0)))
670
}
671
db_reset(&bisectQuery);
672
}
673
drawDetailEllipsis = (tmFlags & (TIMELINE_COMPACT))!=0;
674
db_column_blob(pQuery, commentColumn, &comment);
675
if( tmFlags & TIMELINE_COMPACT ){
676
@ <span class='timelineCompactComment' data-id='%d(rid)'>
677
}else{
678
@ <span class='timeline%s(zStyle)Comment'>
679
}
680
if( (tmFlags & TIMELINE_CLASSIC)!=0 ){
681
if( zType[0]=='c' ){
682
hyperlink_to_version(zUuid);
683
if( isLeaf ){
684
if( has_closed_tag(rid) ){
685
@ <span class="timelineLeaf">Closed-Leaf:</span>
686
}else{
687
@ <span class="timelineLeaf">Leaf:</span>
688
}
689
}
690
}else if( zType[0]=='e' && tagid ){
691
hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
692
}else if( (tmFlags & TIMELINE_ARTID)!=0 ){
693
hyperlink_to_version(zUuid);
694
}
695
if( tmFlags & TIMELINE_SHOWRID ){
696
int srcId = delta_source_rid(rid);
697
if( srcId ){
698
@ (%z(href("%R/deltachain/%d",rid))%d(rid)&larr;%d(srcId)</a>)
699
}else{
700
@ (%z(href("%R/deltachain/%d",rid))%d(rid)</a>)
701
}
702
}
703
}
704
if( zType[0]!='c' ){
705
/* Comments for anything other than a check-in are generated by
706
** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
707
if( zType[0]=='w' ){
708
const char *zCom = blob_str(&comment);
709
/* Except, the comments generated by "fossil rebuild" for a wiki
710
** page edit consist of a single character '-', '+', or ':' (to
711
** indicate "deleted", "added", or "edited") followed by the
712
** raw wiki page name. We have to generate an appropriate
713
** comment on-the-fly
714
*/
715
wiki_hyperlink_override(zUuid);
716
if( zCom[0]=='-' ){
717
@ Deleted wiki page "%z(href("%R/whistory?name=%t",zCom+1))\
718
@ %h(zCom+1)</a>"
719
}else if( (tmFlags & TIMELINE_REFS)!=0
720
&& (zCom[0]=='+' || zCom[0]==':') ){
721
@ Wiki page "%z(href("%R/wiki?name=%t",zCom+1))%h(zCom+1)</a>"
722
}else if( zCom[0]=='+' ){
723
@ Added wiki page "%z(href("%R/wiki?name=%t",zCom+1))%h(zCom+1)</a>"
724
}else if( zCom[0]==':' ){
725
@ %z(href("%R/wdiff?id=%!S",zUuid))Changes</a> to wiki page
726
@ "%z(href("%R/wiki?name=%t",zCom+1))%h(zCom+1)</a>"
727
}else{
728
/* Assume this is an attachment message. It _might_ also
729
** be a legacy-format wiki log entry, in which case it
730
** will simply be rendered in the older format. */
731
wiki_convert(&comment, 0, WIKI_INLINE);
732
}
733
wiki_hyperlink_override(0);
734
}else{
735
if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){
736
blob_truncate_utf8(&comment, mxWikiLen);
737
blob_append(&comment, "...", 3);
738
}
739
wiki_convert(&comment, 0, WIKI_INLINE);
740
}
741
}else{
742
if( bCommentGitStyle ){
743
/* Truncate comment at first blank line */
744
int ii, jj;
745
int n = blob_size(&comment);
746
char *z = blob_str(&comment);
747
for(ii=0; ii<n; ii++){
748
if( z[ii]=='\n' ){
749
for(jj=ii+1; jj<n && z[jj]!='\n' && fossil_isspace(z[jj]); jj++){}
750
if( z[jj]=='\n' ) break;
751
}
752
}
753
z[ii] = 0;
754
cgi_printf("%W",z);
755
}else if( mxWikiLen>0 && (int)blob_size(&comment)>mxWikiLen ){
756
blob_truncate_utf8(&comment, mxWikiLen);
757
blob_append(&comment, "...", 3);
758
@ %W(blob_str(&comment))
759
drawDetailEllipsis = 0;
760
}else{
761
cgi_printf("%W",blob_str(&comment));
762
}
763
}
764
765
@ </span>
766
blob_reset(&comment);
767
768
/* Generate extra information and hyperlinks that follow the comment.
769
** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
770
*/
771
if( drawDetailEllipsis ){
772
@ <span class='timelineEllipsis' id='ellipsis-%d(rid)' \
773
@ data-id='%d(rid)'>...</span>
774
}
775
if( tmFlags & TIMELINE_COLUMNAR ){
776
if( !isSelectedOrCurrent ){
777
@ <td class="timelineDetailCell%s(zExtraClass)" id='md%d(gidx)'>
778
}else{
779
@ <td class="timelineDetailCell%s(zExtraClass)">
780
}
781
}
782
if( tmFlags & TIMELINE_COMPACT ){
783
cgi_printf("<span class='clutter' id='detail-%d'>",rid);
784
}
785
cgi_printf("<span class='timeline%sDetail'>", zStyle);
786
xExtra(pQuery, tmFlags, zThisUser, zThisTag);
787
if( tmFlags & TIMELINE_COMPACT ){
788
@ </span></span>
789
}else{
790
@ </span>
791
}
792
793
/* Generate the file-change list if requested */
794
if( (tmFlags & (TIMELINE_FCHANGES|TIMELINE_FRENAMES))!=0
795
&& zType[0]=='c' && g.perm.Hyperlink
796
){
797
int inUl = 0;
798
if( !fchngQueryInit ){
799
db_prepare(&fchngQuery,
800
"SELECT pid,"
801
" fid,"
802
" (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
803
" (SELECT uuid FROM blob WHERE rid=fid),"
804
" (SELECT uuid FROM blob WHERE rid=pid),"
805
" (SELECT name FROM filename WHERE fnid=mlink.pfnid) AS oldnm"
806
" FROM mlink"
807
" WHERE mid=:mid AND (pid!=fid OR pfnid>0)"
808
" AND (fid>0 OR"
809
" fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=:mid))"
810
" AND NOT mlink.isaux"
811
" ORDER BY 3 /*sort*/"
812
);
813
fchngQueryInit = 1;
814
}
815
db_bind_int(&fchngQuery, ":mid", rid);
816
while( db_step(&fchngQuery)==SQLITE_ROW ){
817
const char *zFilename = db_column_text(&fchngQuery, 2);
818
int isNew = db_column_int(&fchngQuery, 0)<=0;
819
int isMergeNew = db_column_int(&fchngQuery, 0)<0;
820
int fid = db_column_int(&fchngQuery, 1);
821
int isDel = fid==0;
822
const char *zOldName = db_column_text(&fchngQuery, 5);
823
const char *zOld = db_column_text(&fchngQuery, 4);
824
const char *zNew = db_column_text(&fchngQuery, 3);
825
const char *zUnpub = "";
826
char *zA;
827
char *zId;
828
if( !inUl ){
829
@ <ul class="filelist">
830
inUl = 1;
831
}
832
if( tmFlags & TIMELINE_SHOWRID ){
833
int srcId = delta_source_rid(fid);
834
if( srcId ){
835
zId = mprintf(" (%z%d&larr;%d</a>) ",
836
href("%R/deltachain/%d", fid), fid, srcId);
837
}else{
838
zId = mprintf(" (%z%d</a>) ",
839
href("%R/deltachain/%d", fid), fid);
840
}
841
}else{
842
zId = fossil_strdup("");
843
}
844
if( (tmFlags & TIMELINE_FRENAMES)!=0 ){
845
if( !isNew && !isDel && zOldName!=0 ){
846
@ <li> %h(zOldName) &rarr; %h(zFilename)%s(zId)
847
}
848
continue;
849
}
850
zA = href("%R/artifact/%!S",fid?zNew:zOld);
851
if( content_is_private(fid) ){
852
zUnpub = UNPUB_TAG;
853
}
854
if( isNew ){
855
@ <li> %s(zA)%h(zFilename)</a>%s(zId) %s(zUnpub)
856
if( isMergeNew ){
857
@ (added by merge)
858
}else{
859
@ (new file)
860
}
861
@ &nbsp; %z(href("%R/artifact/%!S",zNew))[view]</a></li>
862
}else if( isDel ){
863
@ <li> %s(zA)%h(zFilename)</a> (deleted)</li>
864
}else if( fossil_strcmp(zOld,zNew)==0 && zOldName!=0 ){
865
@ <li> %h(zOldName) &rarr; %s(zA)%h(zFilename)</a>%s(zId)
866
@ %s(zUnpub) %z(href("%R/artifact/%!S",zNew))[view]</a></li>
867
}else{
868
if( zOldName!=0 ){
869
@ <li>%h(zOldName) &rarr; %s(zA)%h(zFilename)%s(zId)</a> %s(zUnpub)
870
}else{
871
@ <li>%s(zA)%h(zFilename)</a>%s(zId) &nbsp; %s(zUnpub)
872
}
873
@ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]</a></li>
874
}
875
fossil_free(zA);
876
fossil_free(zId);
877
}
878
db_reset(&fchngQuery);
879
if( inUl ){
880
@ </ul>
881
}
882
}
883
884
/* Show the complete text of forum messages */
885
if( (tmFlags & (TIMELINE_FORUMTXT))!=0
886
&& zType[0]=='f' && g.perm.Hyperlink
887
&& (!content_is_private(rid) || g.perm.ModForum)
888
){
889
Manifest *pPost = manifest_get(rid, CFTYPE_FORUM, 0);
890
if( pPost ){
891
const char *zClass = "forumTimeline";
892
if( forum_rid_has_been_edited(rid) ){
893
zClass = "forumTimeline forumObs";
894
}
895
forum_render(0, pPost->zMimetype, pPost->zWiki, zClass, 1);
896
manifest_destroy(pPost);
897
}
898
}
899
}
900
if( suppressCnt ){
901
@ <span class="timelineDisabled">... %d(suppressCnt) similar
902
@ event%s(suppressCnt>1?"s":"") omitted.</span>
903
suppressCnt = 0;
904
}
905
if( pendingEndTr ){
906
@ </td></tr>
907
}
908
if( pGraph ){
909
graph_finish(pGraph, pLeftBranch, tmFlags);
910
if( pGraph->nErr ){
911
graph_free(pGraph);
912
pGraph = 0;
913
}else{
914
@ <tr class="timelineBottom" id="btm-%d(iTableId)">\
915
@ <td></td><td></td><td></td></tr>
916
}
917
}
918
@ </table>
919
if( fchngQueryInit ) db_finalize(&fchngQuery);
920
timeline_output_graph_javascript(pGraph, tmFlags, iTableId);
921
}
922
923
/*
924
** Change the RGB background color given in the argument in a foreground
925
** color with the same hue.
926
*/
927
static const char *bg_to_fg(const char *zIn){
928
int i;
929
unsigned int x[3];
930
unsigned int mx = 0;
931
static int whiteFg = -1;
932
static char zRes[10];
933
if( strlen(zIn)!=7 || zIn[0]!='#' ) return zIn;
934
zIn++;
935
for(i=0; i<3; i++){
936
x[i] = hex_digit_value(zIn[0])*16 + hex_digit_value(zIn[1]);
937
zIn += 2;
938
if( x[i]>mx ) mx = x[i];
939
}
940
if( whiteFg<0 ) whiteFg = skin_detail_boolean("white-foreground");
941
if( whiteFg ){
942
/* Make the color lighter */
943
static const unsigned int t = 215;
944
if( mx<t ) for(i=0; i<3; i++) x[i] += t - mx;
945
}else{
946
/* Make the color darker */
947
static const unsigned int t = 128;
948
if( mx>t ){
949
for(i=0; i<3; i++){
950
x[i] = x[i]>=mx-t ? x[i] - (mx-t) : 0;
951
}
952
}
953
}
954
sqlite3_snprintf(sizeof(zRes),zRes,"#%02x%02x%02x",x[0],x[1],x[2]);
955
return zRes;
956
}
957
958
/*
959
** Generate all of the necessary javascript to generate a timeline
960
** graph.
961
*/
962
void timeline_output_graph_javascript(
963
GraphContext *pGraph, /* The graph to be displayed */
964
int tmFlags, /* Flags that control rendering */
965
int iTableId /* Which graph is this for */
966
){
967
if( pGraph && pGraph->nErr==0 ){
968
GraphRow *pRow;
969
int i;
970
char cSep;
971
int iRailPitch; /* Pixels between consecutive rails */
972
int showArrowheads; /* True to draw arrowheads. False to omit. */
973
int circleNodes; /* True for circle nodes. False for square nodes */
974
int colorGraph; /* Use colors for graph lines */
975
int iTopRow; /* Index of the top row of the graph */
976
int fileDiff; /* True for file diff. False for check-in diff */
977
int omitDescenders; /* True to omit descenders */
978
int scrollToSelect; /* True to scroll to the selection */
979
int dwellTimeout; /* Milliseconds to wait for tooltips to show */
980
int closeTimeout; /* Milliseconds to wait for tooltips to close */
981
u8 *aiMap; /* The rail map */
982
u8 bNoGraph; /* True to show a minimal graph */
983
984
bNoGraph = (tmFlags & TIMELINE_GRAPH)==0;
985
iRailPitch = atoi(PD("railpitch","0"));
986
showArrowheads = skin_detail_boolean("timeline-arrowheads");
987
circleNodes = skin_detail_boolean("timeline-circle-nodes");
988
colorGraph = skin_detail_boolean("timeline-color-graph-lines");
989
iTopRow = pGraph->pFirst ? pGraph->pFirst->idx : 0;
990
omitDescenders = (tmFlags & TIMELINE_DISJOINT)!=0;
991
fileDiff = (tmFlags & TIMELINE_FILEDIFF)!=0;
992
scrollToSelect = (tmFlags & TIMELINE_NOSCROLL)==0;
993
dwellTimeout = atoi(db_get("timeline-dwelltime","100"));
994
closeTimeout = atoi(db_get("timeline-closetime","250"));
995
@ <script id='timeline-data-%d(iTableId)' type='application/json'>{
996
@ "iTableId": %d(iTableId),
997
@ "circleNodes": %d(circleNodes),
998
@ "showArrowheads": %d(showArrowheads),
999
@ "iRailPitch": %d(iRailPitch),
1000
@ "colorGraph": %d(colorGraph),
1001
@ "nomo": %d(PB("nomo")),
1002
@ "iTopRow": %d(iTopRow),
1003
@ "omitDescenders": %d(omitDescenders),
1004
@ "fileDiff": %d(fileDiff),
1005
@ "scrollToSelect": %d(scrollToSelect),
1006
@ "nrail": %d(bNoGraph?1:pGraph->mxRail+1),
1007
@ "baseUrl": "%R",
1008
@ "dwellTimeout": %d(dwellTimeout),
1009
@ "closeTimeout": %d(closeTimeout),
1010
@ "hashDigits": %d(hash_digits(1)),
1011
@ "bottomRowId": "btm-%d(iTableId)",
1012
if( pGraph->nRow==0 ){
1013
@ "rowinfo": null
1014
}else{
1015
@ "rowinfo": [
1016
}
1017
1018
/* the rowinfo[] array contains all the information needed to generate
1019
** the graph. Each entry contains information for a single row:
1020
**
1021
** id: The id of the <div> element for the row. This is an integer.
1022
** to get an actual id, prepend "m" to the integer. The top node
1023
** is iTopRow and numbers increase moving down the timeline.
1024
** bg: The background color for this row
1025
** r: The "rail" that the node for this row sits on. The left-most
1026
** rail is 0 and the number increases to the right.
1027
** d: If exists and true then there is a "descender" - an arrow
1028
** coming from the bottom of the page or further down on the page
1029
** straight up to this node.
1030
** mo: "merge-out". If it exists, this is the rail position
1031
** for the upward portion of a merge arrow. The merge arrow goes as
1032
** a solid normal merge line up to the row identified by "mu" and
1033
** then as a dashed cherrypick merge line up further to "cu".
1034
** If this value is omitted if there are no merge children.
1035
** mu: The id of the row which is the top of the merge-out arrow.
1036
** Only exists if "mo" exists.
1037
** cu: Extend the mu merge arrow up to this row as a cherrypick
1038
** merge line, if this value exists.
1039
** u: Draw a thick child-line out of the top of this node and up to
1040
** the node with an id equal to this value. 0 if it is straight to
1041
** the top of the page or just up a little ways, -1 if there is
1042
** no thick-line riser (if the node is a leaf).
1043
** sb: Draw a dotted child-line out of the top of this node up to the
1044
** node with the id equal to the value. This is like "u" except
1045
** that the line is dotted instead of solid and has no arrow.
1046
** Mnemonic: "Same Branch".
1047
** f: 0x01: a leaf node, 0x02: a closed leaf node.
1048
** au: An array of integers that define thick-line risers for branches.
1049
** The integers are in pairs. For each pair, the first integer is
1050
** is the rail on which the riser should run and the second integer
1051
** is the id of the node up to which the riser should run. If there
1052
** are no risers, this array does not exist.
1053
** mi: "merge-in". An array of integer rail positions from which
1054
** merge arrows should be drawn into this node. If the value is
1055
** negative, then the rail position is -1-mi[] and a thin merge-arrow
1056
** descender is drawn to the bottom of the screen. This array is
1057
** omitted if there are no inbound merges.
1058
** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
1059
** omitted if there are no cherrypick merges.
1060
** h: The artifact hash of the object being graphed
1061
* br: The branch to which the artifact belongs
1062
*/
1063
aiMap = pGraph->aiRailMap;
1064
for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
1065
int k = 0;
1066
cgi_printf("{\"id\":%d,", pRow->idx);
1067
cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
1068
if( bNoGraph ){
1069
cgi_printf("\"r\":0,"); /* Chng to ":-1" to omit node circles */
1070
}else{
1071
cgi_printf("\"r\":%d,", pRow->iRail>=0 ? aiMap[pRow->iRail] : -1);
1072
}
1073
if( pRow->bDescender && !bNoGraph ){
1074
cgi_printf("\"d\":%d,", pRow->bDescender);
1075
}
1076
if( pRow->mergeOut>=0 ){
1077
cgi_printf("\"mo\":%d,", aiMap[pRow->mergeOut]);
1078
if( pRow->mergeUpto==0 ) pRow->mergeUpto = pRow->idx;
1079
cgi_printf("\"mu\":%d,", pRow->mergeUpto);
1080
if( pRow->cherrypickUpto>0 && pRow->cherrypickUpto<=pRow->mergeUpto ){
1081
cgi_printf("\"cu\":%d,", pRow->cherrypickUpto);
1082
}
1083
}
1084
if( bNoGraph ){
1085
cgi_printf("\"u\":-1,");
1086
}else if( pRow->isStepParent ){
1087
cgi_printf("\"sb\":%d,", pRow->aiRiser[pRow->iRail]);
1088
}else{
1089
cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
1090
}
1091
k = 0;
1092
if( pRow->isLeaf ) k |= 1;
1093
if( pRow->isLeaf & 2) k |= 2;
1094
cgi_printf("\"f\":%d,",k);
1095
for(i=k=0; i<GR_MAX_RAIL; i++){
1096
if( i==pRow->iRail ) continue;
1097
if( pRow->aiRiser[i]>0 ){
1098
if( k==0 ){
1099
cgi_printf("\"au\":");
1100
cSep = '[';
1101
}
1102
k++;
1103
cgi_printf("%c%d,%d", cSep, aiMap[i], pRow->aiRiser[i]);
1104
cSep = ',';
1105
}
1106
}
1107
if( k ){
1108
cgi_printf("],");
1109
}
1110
if( colorGraph && pRow->zBgClr[0]=='#' ){
1111
cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
1112
}
1113
/* mi */
1114
for(i=k=0; i<GR_MAX_RAIL; i++){
1115
if( pRow->mergeIn[i]==1 ){
1116
int mi = aiMap[i];
1117
if( (pRow->mergeDown >> i) & 1 ) mi = -1-mi;
1118
if( k==0 ){
1119
cgi_printf("\"mi\":");
1120
cSep = '[';
1121
}
1122
k++;
1123
cgi_printf("%c%d", cSep, mi);
1124
cSep = ',';
1125
}
1126
}
1127
if( k ) cgi_printf("],");
1128
/* ci */
1129
for(i=k=0; i<GR_MAX_RAIL; i++){
1130
if( pRow->mergeIn[i]==2 ){
1131
int mi = aiMap[i];
1132
if( (pRow->cherrypickDown >> i) & 1 ) mi = -mi;
1133
if( k==0 ){
1134
cgi_printf("\"ci\":");
1135
cSep = '[';
1136
}
1137
k++;
1138
cgi_printf("%c%d", cSep, mi);
1139
cSep = ',';
1140
}
1141
}
1142
if( k ) cgi_printf("],");
1143
cgi_printf("\"br\":\"%j\",", pRow->zBranch ? pRow->zBranch : "");
1144
cgi_printf("\"h\":\"%!S\"}%s",
1145
pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
1146
}
1147
@ }</script>
1148
builtin_request_js("graph.js");
1149
builtin_request_js("copybtn.js"); /* Required by graph.js */
1150
graph_free(pGraph);
1151
}
1152
}
1153
1154
/*
1155
** Create a temporary table suitable for storing timeline data.
1156
*/
1157
static void timeline_temp_table(void){
1158
static const char zSql[] =
1159
@ CREATE TEMP TABLE IF NOT EXISTS timeline(
1160
@ rid INTEGER PRIMARY KEY,
1161
@ uuid TEXT,
1162
@ timestamp TEXT,
1163
@ comment TEXT,
1164
@ user TEXT,
1165
@ isleaf BOOLEAN,
1166
@ bgcolor TEXT,
1167
@ etype TEXT,
1168
@ taglist TEXT,
1169
@ tagid INTEGER,
1170
@ short TEXT,
1171
@ sortby REAL
1172
@ )
1173
;
1174
db_multi_exec("%s", zSql/*safe-for-%s*/);
1175
}
1176
1177
/*
1178
** Return a pointer to a constant string that forms the basis
1179
** for a timeline query for the WWW interface.
1180
*/
1181
const char *timeline_query_for_www(void){
1182
static const char zBase[] =
1183
@ SELECT
1184
@ blob.rid AS blobRid,
1185
@ uuid AS uuid,
1186
@ datetime(event.mtime,toLocal()) AS timestamp,
1187
@ coalesce(ecomment, comment) AS comment,
1188
@ coalesce(euser, user) AS user,
1189
@ blob.rid IN leaf AS leaf,
1190
@ bgcolor AS bgColor,
1191
@ event.type AS eventType,
1192
@ (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref
1193
@ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
1194
@ AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags,
1195
@ tagid AS tagid,
1196
@ brief AS brief,
1197
@ event.mtime AS mtime
1198
@ FROM event CROSS JOIN blob
1199
@ WHERE blob.rid=event.objid
1200
;
1201
return zBase;
1202
}
1203
1204
/*
1205
** zDate is a localtime date. Insert records into the
1206
** "timeline" table to cause <hr> to be inserted on zDate.
1207
*/
1208
static int timeline_add_divider(double rDate){
1209
int rid = db_int(-1,
1210
"SELECT rid FROM timeline ORDER BY abs(sortby-%.16g) LIMIT 1", rDate
1211
);
1212
if( rid>0 ) return rid;
1213
db_multi_exec(
1214
"INSERT INTO timeline(rid,sortby,etype) VALUES(-1,%.16g,'div')",
1215
rDate
1216
);
1217
return -1;
1218
}
1219
1220
/*
1221
** Return all possible names for file zUuid.
1222
*/
1223
char *names_of_file(const char *zUuid){
1224
Stmt q;
1225
Blob out;
1226
const char *zSep = "";
1227
db_prepare(&q,
1228
"SELECT DISTINCT filename.name FROM mlink, filename"
1229
" WHERE mlink.fid=(SELECT rid FROM blob WHERE uuid=%Q)"
1230
" AND filename.fnid=mlink.fnid",
1231
zUuid
1232
);
1233
blob_zero(&out);
1234
while( db_step(&q)==SQLITE_ROW ){
1235
const char *zFN = db_column_text(&q, 0);
1236
blob_appendf(&out, "%s%z%h</a>", zSep,
1237
href("%R/finfo?name=%t&m=%!S", zFN, zUuid), zFN);
1238
zSep = " or ";
1239
}
1240
db_finalize(&q);
1241
return blob_str(&out);
1242
}
1243
1244
1245
/*
1246
** Add the select/option box to the timeline submenu that is used to
1247
** set the y= parameter that determines which elements to display
1248
** on the timeline.
1249
*/
1250
static void timeline_y_submenu(int isDisabled){
1251
static int i = 0;
1252
static const char *az[16];
1253
if( i==0 ){
1254
az[0] = "all";
1255
az[1] = "Any Type";
1256
i = 2;
1257
if( g.perm.Read ){
1258
az[i++] = "ci";
1259
az[i++] = "Check-ins";
1260
az[i++] = "g";
1261
az[i++] = "Tags";
1262
}
1263
if( g.perm.RdWiki ){
1264
az[i++] = "e";
1265
az[i++] = "Tech Notes";
1266
}
1267
if( g.perm.RdTkt ){
1268
az[i++] = "t";
1269
az[i++] = "Tickets";
1270
az[i++] = "n";
1271
az[i++] = "New Tickets";
1272
}
1273
if( g.perm.RdWiki ){
1274
az[i++] = "w";
1275
az[i++] = "Wiki";
1276
}
1277
if( g.perm.RdForum ){
1278
az[i++] = "f";
1279
az[i++] = "Forum";
1280
}
1281
assert( i<=count(az) );
1282
}
1283
if( i>2 ){
1284
style_submenu_multichoice("y", i/2, az, isDisabled);
1285
}
1286
}
1287
1288
/*
1289
** SETTING: timeline-default-style width=5 default=m
1290
**
1291
** This setting determines the default "view style" for timelines.
1292
** The setting should be a single character, one of the following:
1293
**
1294
** c Compact
1295
** j Columnar
1296
** m Modern
1297
** s Simple
1298
** v Verbose
1299
** x Classic
1300
**
1301
** The default value is m (Modern).
1302
*/
1303
1304
/*
1305
** Return the default value for the "ss" cookie or query parameter.
1306
** The "ss" cookie determines the graph style. See the
1307
** timeline_view_styles[] global constant for a list of choices.
1308
*/
1309
const char *timeline_default_ss(void){
1310
static const char *zSs = 0;
1311
if( zSs==0 ) zSs = db_get("timeline-default-style","m");
1312
return zSs;
1313
}
1314
1315
/*
1316
** Convert the current "ss" display preferences cookie into an
1317
** appropriate TIMELINE_* flag
1318
*/
1319
int timeline_ss_cookie(void){
1320
int tmFlags;
1321
const char *v = cookie_value("ss",0);
1322
if( v==0 ) v = timeline_default_ss();
1323
switch( v[0] ){
1324
case 'c': tmFlags = TIMELINE_COMPACT; break;
1325
case 'v': tmFlags = TIMELINE_VERBOSE; break;
1326
case 'j': tmFlags = TIMELINE_COLUMNAR; break;
1327
case 'x': tmFlags = TIMELINE_CLASSIC; break;
1328
case 's': tmFlags = TIMELINE_SIMPLE; break;
1329
default: tmFlags = TIMELINE_MODERN; break;
1330
}
1331
return tmFlags;
1332
}
1333
1334
/* Available timeline display styles, together with their y= query
1335
** parameter names.
1336
*/
1337
const char *const timeline_view_styles[] = {
1338
"m", "Modern View",
1339
"j", "Columnar View",
1340
"c", "Compact View",
1341
"s", "Simple View",
1342
"v", "Verbose View",
1343
"x", "Classic View",
1344
};
1345
#if INTERFACE
1346
# define N_TIMELINE_VIEW_STYLE 6
1347
#endif
1348
1349
/*
1350
** Add the select/option box to the timeline submenu that is used to
1351
** set the ss= parameter that determines the viewing mode.
1352
**
1353
** Return the TIMELINE_* value appropriate for the view-style.
1354
*/
1355
int timeline_ss_submenu(void){
1356
cookie_link_parameter("ss","ss",timeline_default_ss());
1357
style_submenu_multichoice("ss",
1358
N_TIMELINE_VIEW_STYLE,
1359
timeline_view_styles, 0);
1360
return timeline_ss_cookie();
1361
}
1362
1363
/*
1364
** If the zChng string is not NULL, then it should be a comma-separated
1365
** list of glob patterns for filenames. Add an term to the WHERE clause
1366
** for the SQL statement under construction that excludes any check-in that
1367
** does not modify one or more files matching the globs.
1368
*/
1369
static void addFileGlobExclusion(
1370
const char *zChng, /* The filename GLOB list */
1371
Blob *pSql /* The SELECT statement under construction */
1372
){
1373
if( zChng==0 ) return;
1374
blob_append_sql(pSql," AND event.objid IN ("
1375
"SELECT mlink.mid FROM mlink, filename\n"
1376
" WHERE mlink.fnid=filename.fnid\n"
1377
" AND %s)",
1378
glob_expr("filename.name", mprintf("\"%s\"", zChng)));
1379
}
1380
static void addFileGlobDescription(
1381
const char *zChng, /* The filename GLOB list */
1382
Blob *pDescription /* Result description */
1383
){
1384
if( zChng==0 ) return;
1385
blob_appendf(pDescription, " that include changes to files matching '%h'",
1386
zChng);
1387
}
1388
1389
/*
1390
** If zChng is not NULL, then use it as a comma-separated list of
1391
** glob patterns for filenames, and remove from the "ok" table any
1392
** check-ins that do not modify one or more of the files identified
1393
** by zChng.
1394
*/
1395
static void removeFileGlobFromOk(
1396
const char *zChng /* The filename GLOB list */
1397
){
1398
if( zChng==0 ) return;
1399
db_multi_exec(
1400
"DELETE FROM ok WHERE rid NOT IN (\n"
1401
" SELECT mlink.mid FROM mlink, filename\n"
1402
" WHERE mlink.fnid=filename.fnid\n"
1403
" AND %z);\n",
1404
glob_expr("filename.name", zChng)
1405
);
1406
}
1407
1408
/*
1409
** Similar to fossil_expand_datetime()
1410
**
1411
** Add missing "-" characters into a date/time. Examples:
1412
**
1413
** 20190419 => 2019-04-19
1414
** 201904 => 2019-04
1415
*/
1416
const char *timeline_expand_datetime(const char *zIn, int *pbZulu){
1417
static char zEDate[16];
1418
int n = (int)strlen(zIn);
1419
int i, j;
1420
1421
/* These forms are recognized:
1422
**
1423
** (1) YYYYMMDD
1424
** (2) YYYYMM
1425
** (3) YYYYWW
1426
*/
1427
if( n && (zIn[n-1]=='Z' || zIn[n-1]=='z') ){
1428
n--;
1429
if( pbZulu ) *pbZulu = 1;
1430
}else{
1431
if( pbZulu ) *pbZulu = 0;
1432
}
1433
if( n!=8 && n!=6 ) return zIn;
1434
1435
/* Every character must be a digit */
1436
for(i=0; i<n && fossil_isdigit(zIn[i]); i++){}
1437
if( i!=n ) return zIn;
1438
1439
/* Expand the date */
1440
for(i=j=0; i<n; i++){
1441
if( j==4 || j==7 ) zEDate[j++] = '-';
1442
zEDate[j++] = zIn[i];
1443
}
1444
zEDate[j] = 0;
1445
1446
/* It looks like this may be a date. Return it with punctuation added. */
1447
return zEDate;
1448
}
1449
1450
/*
1451
** Check to see if the argument is a date-span for the ymd= query
1452
** parameter. A valid date-span is of the form:
1453
**
1454
** 0123456789 123456 <-- index
1455
** YYYYMMDD-YYYYMMDD
1456
**
1457
** with an optional "Z" timeline modifier at the end. Return true if
1458
** the input is a valid date space and false if not.
1459
*/
1460
static int timeline_is_datespan(const char *zDay){
1461
size_t n = strlen(zDay);
1462
int i, d, m;
1463
1464
if( n<17 || n>18 ) return 0;
1465
if( n==18 ){
1466
if( zDay[17]!='Z' && zDay[17]!='z' ) return 0;
1467
n--;
1468
}
1469
if( zDay[8]!='-' ) return 0;
1470
for(i=0; i<17 && (fossil_isdigit(zDay[i]) || i==8); i++){}
1471
if( i!=17 ) return 0;
1472
i = atoi(zDay);
1473
d = i%100;
1474
if( d<1 || d>31 ) return 0;
1475
m = (i/100)%100;
1476
if( m<1 || m>12 ) return 0;
1477
i = atoi(zDay+9);
1478
d = i%100;
1479
if( d<1 || d>31 ) return 0;
1480
m = (i/100)%100;
1481
if( m<1 || m>12 ) return 0;
1482
return 1;
1483
}
1484
1485
/*
1486
** Find the first check-in encountered with a particular tag
1487
** when moving either forwards are backwards in time from a
1488
** particular starting point (iFrom). Return the rid of that
1489
** first check-in. If there are no check-ins in the descendant
1490
** or ancestor set of check-in iFrom that match the tag, then
1491
** return 0.
1492
*/
1493
static int timeline_endpoint(
1494
int iFrom, /* Starting point */
1495
const char *zEnd, /* Tag we are searching for */
1496
int bForward /* 1: forwards in time (descendants) 0: backwards */
1497
){
1498
int tagId;
1499
int endId = 0;
1500
Stmt q;
1501
int ans = 0;
1502
1503
tagId = db_int(0, "SELECT tagid FROM tag WHERE tagname='sym-%q'", zEnd);
1504
if( tagId==0 ){
1505
endId = symbolic_name_to_rid(zEnd, "ci");
1506
if( endId==0 ) return 0;
1507
}
1508
db_pause_dml_log();
1509
if( bForward ){
1510
if( tagId ){
1511
db_prepare(&q,
1512
"WITH RECURSIVE dx(id,mtime) AS ("
1513
" SELECT %d, event.mtime FROM event WHERE objid=%d"
1514
" UNION"
1515
" SELECT plink.cid, plink.mtime"
1516
" FROM dx, plink"
1517
" WHERE plink.pid=dx.id"
1518
" AND plink.mtime<=(SELECT max(event.mtime) FROM tagxref, event"
1519
" WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
1520
" AND event.objid=tagxref.rid)"
1521
" ORDER BY plink.mtime)"
1522
"SELECT id FROM dx, tagxref"
1523
" WHERE tagid=%d AND tagtype>0 AND rid=id"
1524
" ORDER BY dx.mtime LIMIT 1",
1525
iFrom, iFrom, tagId, tagId
1526
);
1527
}else{
1528
db_prepare(&q,
1529
"WITH RECURSIVE dx(id,mtime) AS ("
1530
" SELECT %d, event.mtime FROM event WHERE objid=%d"
1531
" UNION"
1532
" SELECT plink.cid, plink.mtime"
1533
" FROM dx, plink"
1534
" WHERE plink.pid=dx.id"
1535
" AND plink.mtime<=(SELECT mtime FROM event WHERE objid=%d)"
1536
" ORDER BY plink.mtime)"
1537
"SELECT id FROM dx WHERE id=%d",
1538
iFrom, iFrom, endId, endId
1539
);
1540
}
1541
}else{
1542
if( tagId ){
1543
db_prepare(&q,
1544
"WITH RECURSIVE dx(id,mtime) AS ("
1545
" SELECT %d, event.mtime FROM event WHERE objid=%d"
1546
" UNION"
1547
" SELECT plink.pid, event.mtime"
1548
" FROM dx, plink, event"
1549
" WHERE plink.cid=dx.id AND event.objid=plink.pid"
1550
" AND event.mtime>=(SELECT min(event.mtime) FROM tagxref, event"
1551
" WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
1552
" AND event.objid=tagxref.rid)"
1553
" ORDER BY event.mtime DESC)"
1554
"SELECT id FROM dx, tagxref"
1555
" WHERE tagid=%d AND tagtype>0 AND rid=id"
1556
" ORDER BY dx.mtime DESC LIMIT 1",
1557
iFrom, iFrom, tagId, tagId
1558
);
1559
}else{
1560
db_prepare(&q,
1561
"WITH RECURSIVE dx(id,mtime) AS ("
1562
" SELECT %d, event.mtime FROM event WHERE objid=%d"
1563
" UNION"
1564
" SELECT plink.pid, event.mtime"
1565
" FROM dx, plink, event"
1566
" WHERE plink.cid=dx.id AND event.objid=plink.pid"
1567
" AND event.mtime>=(SELECT mtime FROM event WHERE objid=%d)"
1568
" ORDER BY event.mtime DESC)"
1569
"SELECT id FROM dx WHERE id=%d",
1570
iFrom, iFrom, endId, endId
1571
);
1572
}
1573
}
1574
if( db_step(&q)==SQLITE_ROW ){
1575
ans = db_column_int(&q, 0);
1576
}
1577
db_finalize(&q);
1578
db_unpause_dml_log();
1579
return ans;
1580
}
1581
1582
/*
1583
** Add to the (temp) table zTab, RID values for every check-in
1584
** identifier found on the zExtra string. Check-in names can be separated
1585
** by commas or by whitespace.
1586
*/
1587
static void add_extra_rids(const char *zTab, const char *zExtra){
1588
int ii;
1589
int rid;
1590
int cnt;
1591
Blob sql;
1592
char *zX;
1593
char *zToDel;
1594
if( zExtra==0 ) return;
1595
cnt = 0;
1596
blob_init(&sql, 0, 0);
1597
zX = zToDel = fossil_strdup(zExtra);
1598
blob_append_sql(&sql, "INSERT OR IGNORE INTO \"%w\" VALUES", zTab);
1599
while( zX[0] ){
1600
char c;
1601
if( zX[0]==',' || zX[0]==' ' ){ zX++; continue; }
1602
for(ii=1; zX[ii] && zX[ii]!=',' && zX[ii]!=' '; ii++){}
1603
c = zX[ii];
1604
zX[ii] = 0;
1605
rid = name_to_rid(zX);
1606
if( rid>0 ){
1607
if( (cnt%10)==4 ){
1608
blob_append_sql(&sql,",\n ");
1609
}else if( cnt>0 ){
1610
blob_append_sql(&sql,",");
1611
}
1612
blob_append_sql(&sql, "(%d)", rid);
1613
cnt++;
1614
}
1615
zX[ii] = c;
1616
zX += ii;
1617
}
1618
if( cnt ) db_exec_sql(blob_sql_text(&sql));
1619
blob_reset(&sql);
1620
fossil_free(zToDel);
1621
}
1622
1623
/*
1624
** COMMAND: test-endpoint
1625
**
1626
** Usage: fossil test-endpoint BASE TAG ?OPTIONS?
1627
**
1628
** Show the first check-in with TAG that is a descendant or ancestor
1629
** of BASE. The first descendant check-in is shown by default. Use
1630
** the --backto to see the first ancestor check-in.
1631
**
1632
** Options:
1633
**
1634
** --backto Show ancestor. Others defaults to descendants.
1635
*/
1636
void timeline_test_endpoint(void){
1637
int bForward = find_option("backto",0,0)==0;
1638
int from_rid;
1639
int ans;
1640
db_find_and_open_repository(0, 0);
1641
verify_all_options();
1642
if( g.argc!=4 ){
1643
usage("BASE-CHECKIN TAG ?--backto?");
1644
}
1645
from_rid = symbolic_name_to_rid(g.argv[2],"ci");
1646
ans = timeline_endpoint(from_rid, g.argv[3], bForward);
1647
if( ans ){
1648
fossil_print("Result: %d (%S)\n", ans, rid_to_uuid(ans));
1649
}else{
1650
fossil_print("No path found\n");
1651
}
1652
}
1653
1654
1655
/*
1656
** WEBPAGE: timeline
1657
**
1658
** Query parameters:
1659
**
1660
** a=TIMEORTAG Show events after TIMEORTAG.
1661
** b=TIMEORTAG Show events before TIMEORTAG.
1662
** c=TIMEORTAG Show events that happen "circa" TIMEORTAG
1663
** cf=FILEHASH Show events around the time of the first use of
1664
** the file with FILEHASH.
1665
** m=TIMEORTAG Highlight the event at TIMEORTAG, or the closest available
1666
** event if TIMEORTAG is not part of the timeline. If
1667
** the t= or r= is used, the m event is added to the timeline
1668
** if it isn't there already.
1669
** x=LIST Show check-ins in the comma- or space-separated LIST
1670
** in addition to check-ins specified by other parameters.
1671
** sel1=TIMEORTAG Highlight the check-in at TIMEORTAG if it is part of
1672
** the timeline. Similar to m= except TIMEORTAG must
1673
** match a check-in that is already in the timeline.
1674
** sel2=TIMEORTAG Like sel1= but use the secondary highlight.
1675
** n=COUNT Maximum number of events. "all" for no limit
1676
** n1=COUNT Same as "n" but doesn't set the display-preference cookie
1677
** Use "n1=COUNT" for a one-time display change
1678
** p=CHECKIN Parents and ancestors of CHECKIN
1679
** bt=PRIOR ... going back to PRIOR
1680
** p2=CKIN2 ... use CKIN2 if CHECKIN is not found
1681
** d=CHECKIN Children and descendants of CHECKIN
1682
** d2=CKIN2 ... Use CKIN2 if CHECKIN is not found
1683
** ft=DESCENDANT ... going forward to DESCENDANT
1684
** dp=CHECKIN Same as 'd=CHECKIN&p=CHECKIN'
1685
** dp2=CKIN2 Same as 'd2=CKIN2&p2=CKIN2'
1686
** df=CHECKIN Same as 'd=CHECKIN&n1=all&nd'. Mnemonic: "Derived From"
1687
** bt=CHECKIN "Back To". Show ancestors going back to CHECKIN
1688
** p=CX ... from CX back to time of CHECKIN
1689
** from=CX ... path from CX back to CHECKIN
1690
** ft=CHECKIN "Forward To": Show descendants forward to CHECKIN
1691
** d=CX ... from CX up to the time of CHECKIN
1692
** from=CX ... path from CX up to CHECKIN
1693
** t=TAG Show only check-ins with the given TAG
1694
** r=TAG Same as 't=TAG&rel'. Mnemonic: "Related"
1695
** tl=TAGLIST Same as 't=TAGLIST&ms=brlist'. Mnemonic: "Tag List"
1696
** rl=TAGLIST Same as 'r=TAGLIST&ms=brlist'. Mnemonic: "Related List"
1697
** ml=TAGLIST Same as 'tl=TAGLIST&mionly'. Mnemonic: "Merge-in List"
1698
** sl=TAGLIST "Sort List". Draw TAGLIST branches ordered left to right.
1699
** rel Show related check-ins as well as those matching t=TAG
1700
** mionly Show related parents but not related children.
1701
** nowiki Do not show wiki associated with branch or tag
1702
** ms=MATCHSTYLE Set tag name match algorithm. One of "exact", "glob",
1703
** "like", or "regexp".
1704
** u=USER Only show items associated with USER
1705
** y=TYPE 'ci', 'w', 't', 'n', 'e', 'f', or 'all'.
1706
** ss=VIEWSTYLE c: "Compact", v: "Verbose", m: "Modern", j: "Columnar",
1707
** x: "Classic".
1708
** advm Use the "Advanced" or "Busy" menu design.
1709
** ng No Graph.
1710
** ncp Omit cherrypick merges
1711
** nd Do not highlight the focus check-in
1712
** nsm Omit the submenu
1713
** nc Omit all graph colors other than highlights
1714
** v Show details of files changed
1715
** vfx Show complete text of forum messages
1716
** f=CHECKIN Family (immediate parents and children) of CHECKIN
1717
** from=CHECKIN Path through common ancestor from CHECKIN...
1718
** to=CHECKIN ... to this
1719
** to2=CHECKIN ... backup name if to= doesn't resolve
1720
** shortest ... pick path with least number of nodes
1721
** rel ... also show related checkins
1722
** min ... hide long sequences along same branch
1723
** bt=PRIOR ... path from CHECKIN back to PRIOR
1724
** ft=LATER ... path from CHECKIN forward to LATER
1725
** me=CHECKIN Most direct path from CHECKIN...
1726
** you=CHECKIN ... to this
1727
** rel ... also show related checkins
1728
** uf=FILE_HASH Show only check-ins that contain the given file version
1729
** All qualifying check-ins are shown unless there is
1730
** also an n= or n1= query parameter.
1731
** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1732
** name matches one of the comma-separated GLOBLIST
1733
** brbg Background color determined by branch name
1734
** ubg Background color determined by user
1735
** deltabg Background color red for delta manifests or green
1736
** for baseline manifests
1737
** namechng Show only check-ins that have filename changes
1738
** forks Show only forks and their children
1739
** cherrypicks Show all cherrypicks
1740
** ym=YYYYMM Show only events for the given year/month
1741
** yw=YYYYWW Show only events for the given week of the given year
1742
** yw=YYYYMMDD Show events for the week that includes the given day
1743
** ymd=YYYYMMDD Show only events on the given day. The use "ymd=now"
1744
** to see all changes for the current week. Add "z" at end
1745
** to divide days at UTC instead of localtime days.
1746
** Use ymd=YYYYMMDD-YYYYMMDD (with optional "z") for a range.
1747
** year=YYYY Show only events on the given year. The use "year=0"
1748
** to see all changes for the current year.
1749
** days=N Show events over the previous N days
1750
** datefmt=N Override the date format: 0=HH:MM, 1=HH:MM:SS,
1751
** 2=YYYY-MM-DD HH:MM:SS, 3=YYMMDD HH:MM, and 4 means "off".
1752
** bisect Show the check-ins that are in the current bisect
1753
** oldestfirst Show events oldest first.
1754
** showid Show RIDs
1755
** showsql Show the SQL used to generate the report
1756
**
1757
** p= and d= can appear individually or together. If either p= or d=
1758
** appear, then u=, y=, a=, and b= are ignored.
1759
**
1760
** If both a= and b= appear then both upper and lower bounds are honored.
1761
**
1762
** When multiple time-related filters are used, e.g. ym, yw, and ymd,
1763
** which one(s) is/are applied is unspecified and may change between
1764
** fossil versions.
1765
**
1766
** CHECKIN or TIMEORTAG can be a check-in hash prefix, or a tag, or the
1767
** name of a branch.
1768
*/
1769
void page_timeline(void){
1770
Stmt q; /* Query used to generate the timeline */
1771
Blob sql; /* text of SQL used to generate timeline */
1772
Blob desc; /* Description of the timeline */
1773
int nEntry; /* Max number of entries on timeline */
1774
int p_rid; /* artifact p and its parents */
1775
int d_rid; /* artifact d and descendants */
1776
int f_rid; /* artifact f and close family */
1777
const char *zUser = P("u"); /* All entries by this user if not NULL */
1778
const char *zType; /* Type of events to display */
1779
const char *zAfter = P("a"); /* Events after this time */
1780
const char *zBefore = P("b"); /* Events before this time */
1781
const char *zCirca = P("c"); /* Events near this time */
1782
const char *zMark = P("m"); /* Mark this event or an event this time */
1783
const char *zTagName = P("t"); /* Show events with this tag */
1784
const char *zBrName = P("r"); /* Equivalent to t=TAG&rel */
1785
int related = PB("rel"); /* Show events related to zTagName */
1786
const char *zMatchStyle = P("ms"); /* Tag/branch match style string */
1787
MatchStyle matchStyle = MS_EXACT; /* Match style code */
1788
const char *zMatchDesc = 0; /* Tag match expression description text */
1789
const char *zError = 0; /* Tag match error string */
1790
const char *zTagSql = 0; /* Tag/branch match SQL expression */
1791
const char *zSearch = P("s"); /* Search string */
1792
const char *zUses = P("uf"); /* Only show check-ins hold this file */
1793
const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */
1794
const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */
1795
char *zYearWeekStart = 0; /* YYYY-MM-DD for start of YYYY-WW */
1796
const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */
1797
const char *zYear = P("year"); /* Events for the year YYYY */
1798
const char *zNDays = P("days"); /* Show events over the previous N days */
1799
int nDays = 0; /* Numeric value for zNDays */
1800
const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1801
int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1802
int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1803
int forkOnly = PB("forks"); /* Show only forks and their children */
1804
int bisectLocal = PB("bisect"); /* Show the check-ins of the bisect */
1805
const char *zBisect = P("bid"); /* Bisect description */
1806
int cpOnly = PB("cherrypicks"); /* Show all cherrypick check-ins */
1807
int tmFlags = 0; /* Timeline flags */
1808
const char *zThisTag = 0; /* Suppress links to this tag */
1809
const char *zThisUser = 0; /* Suppress links to this user */
1810
HQuery url; /* URL for various branch links */
1811
int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
1812
const char *zTo2 = 0;
1813
int to_rid = name_choice("to","to2",&zTo2); /* to= for path timelines */
1814
int bShort = P("shortest")!=0; /* shortest possible path */
1815
int me_rid = name_to_typed_rid(P("me"),"ci"); /* me= for common ancestory */
1816
int you_rid = name_to_typed_rid(P("you"),"ci");/* you= for common ancst */
1817
int pd_rid;
1818
const char *zDPNameP, *zDPNameD; /* Value of p=, d=, or dp= params */
1819
double rBefore, rAfter, rCirca; /* Boundary times */
1820
const char *z;
1821
char *zOlderButton = 0; /* URL for Older button at the bottom */
1822
char *zOlderButtonLabel = 0; /* Label for the Older Button */
1823
char *zNewerButton = 0; /* URL for Newer button at the top */
1824
char *zNewerButtonLabel = 0; /* Label for the Newer button */
1825
int selectedRid = 0; /* Show a highlight on this RID */
1826
int secondaryRid = 0; /* Show secondary highlight */
1827
int disableY = 0; /* Disable type selector on submenu */
1828
int advancedMenu = 0; /* Use the advanced menu design */
1829
char *zPlural; /* Ending for plural forms */
1830
int showCherrypicks = 1; /* True to show cherrypick merges */
1831
int haveParameterN; /* True if n= query parameter present */
1832
int from_to_mode = 0; /* 0: from,to. 1: from,ft 2: from,bt */
1833
int showSql = PB("showsql"); /* True to show the SQL */
1834
Blob allSql; /* Copy of all SQL text */
1835
int bMin = P("min")!=0; /* True if "min" query parameter used */
1836
1837
login_check_credentials();
1838
url_initialize(&url, "timeline");
1839
cgi_query_parameters_to_url(&url);
1840
blob_init(&allSql, 0, 0);
1841
1842
/* The "mionly" query parameter is like "rel", but shows merge-ins only */
1843
if( P("mionly")!=0 ) related = 2;
1844
1845
(void)P_NoBot("ss")
1846
/* "ss" is processed via the udc but at least one spider likes to
1847
** try to SQL inject via this argument, so let's catch that. */;
1848
1849
/* Set number of rows to display */
1850
z = P("n");
1851
if( z!=0 ){
1852
haveParameterN = 1;
1853
cookie_write_parameter("n","n",0);
1854
}else{
1855
const char *z2;
1856
haveParameterN = 0;
1857
cookie_read_parameter("n","n");
1858
z = P("n");
1859
if( z==0 ){
1860
z = db_get("timeline-default-length",0);
1861
}
1862
cgi_replace_query_parameter("n",fossil_strdup(z));
1863
cookie_write_parameter("n","n",0);
1864
z2 = P("n1");
1865
if( z2 ){
1866
haveParameterN = 2;
1867
z = z2;
1868
}
1869
}
1870
if( z ){
1871
if( fossil_strcmp(z,"all")==0 ){
1872
nEntry = 0;
1873
}else{
1874
nEntry = atoi(z);
1875
if( nEntry<=0 ){
1876
z = "50";
1877
nEntry = 50;
1878
}
1879
}
1880
}else{
1881
nEntry = 50;
1882
}
1883
1884
/* Query parameters d=, p=, and f= and variants */
1885
p_rid = name_choice("p","p2", &zDPNameP);
1886
d_rid = name_choice("d","d2", &zDPNameD);
1887
z = P("f");
1888
f_rid = z ? name_to_typed_rid(z,"ci") : 0;
1889
z = P("df");
1890
if( z && (d_rid = name_to_typed_rid(z,"ci"))!=0 ){
1891
nEntry = 0;
1892
useDividers = 0;
1893
cgi_replace_query_parameter("d",fossil_strdup(z));
1894
zDPNameD = zDPNameP = z;
1895
}
1896
if( zChng && zChng[0]==0 ) zChng = 0;
1897
1898
/* Undocumented query parameter to set JS mode */
1899
builtin_set_js_delivery_mode(P("jsmode"),1);
1900
1901
secondaryRid = name_to_typed_rid(P("sel2"),"ci");
1902
selectedRid = name_to_typed_rid(P("sel1"),"ci");
1903
if( from_rid!=0 && to_rid!=0 ){
1904
if( selectedRid==0 ) selectedRid = from_rid;
1905
if( secondaryRid==0 ) secondaryRid = to_rid;
1906
}
1907
tmFlags |= timeline_ss_submenu();
1908
cookie_link_parameter("advm","advm","0");
1909
advancedMenu = atoi(PD("advm","0"));
1910
1911
/* Omit all cherry-pick merge lines if the "ncp" query parameter is
1912
** present or if this repository lacks a "cherrypick" table. */
1913
if( PB("ncp") || !db_table_exists("repository","cherrypick") ){
1914
showCherrypicks = 0;
1915
}
1916
1917
/* To view the timeline, must have permission to read project data.
1918
*/
1919
pd_rid = name_choice("dp","dp2",&zDPNameP);
1920
if( pd_rid ){
1921
p_rid = d_rid = pd_rid;
1922
zDPNameD = zDPNameP;
1923
}
1924
if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum)
1925
|| (bisectLocal && !g.perm.Setup)
1926
){
1927
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
1928
return;
1929
}
1930
if( zBefore || zCirca ){
1931
if( robot_restrict("timelineX") ) return;
1932
}
1933
if( !bisectLocal ){
1934
etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0);
1935
}
1936
cookie_read_parameter("y","y");
1937
zType = P("y");
1938
if( zType==0 ){
1939
zType = g.perm.Read ? "ci" : "all";
1940
cgi_set_parameter("y", zType);
1941
}
1942
if( zType[0]=='a' ||
1943
( g.perm.Read && zType[0]=='c' ) ||
1944
( g.perm.RdTkt && (zType[0]=='t' || zType[0]=='n') ) ||
1945
( g.perm.RdWiki && (zType[0]=='w' || zType[0]=='e') ) ||
1946
( g.perm.RdForum && zType[0]=='f' )
1947
){
1948
cookie_write_parameter("y","y",zType);
1949
}
1950
1951
/* Convert the cf=FILEHASH query parameter into a c=CHECKINHASH value */
1952
if( P("cf")!=0 ){
1953
zCirca = db_text(0,
1954
"SELECT (SELECT uuid FROM blob WHERE rid=mlink.mid)"
1955
" FROM mlink, event"
1956
" WHERE mlink.fid=(SELECT rid FROM blob WHERE uuid LIKE '%q%%')"
1957
" AND event.objid=mlink.mid"
1958
" ORDER BY event.mtime LIMIT 1",
1959
P("cf")
1960
);
1961
}
1962
1963
/* Check for tl=TAGLIST and rl=TAGLIST which are abbreviations for
1964
** t=TAGLIST&ms=brlist and r=TAGLIST&ms=brlist repectively. */
1965
if( zBrName==0 && zTagName==0 ){
1966
const char *z;
1967
const char *zPattern = 0;
1968
if( (z = P("tl"))!=0 ){
1969
zPattern = zTagName = z;
1970
}else if( (z = P("rl"))!=0 ){
1971
zPattern = zBrName = z;
1972
if( related==0 ) related = 1;
1973
}else if( (z = P("ml"))!=0 ){
1974
zPattern = zBrName = z;
1975
if( related==0 ) related = 2;
1976
}
1977
if( zPattern!=0 && zMatchStyle==0 ){
1978
/* If there was no ms= query parameter, set the match style to
1979
** "glob" if the pattern appears to contain GLOB character, or
1980
** "brlist" if it does not. */
1981
if( strpbrk(zPattern,"*[?") ){
1982
zMatchStyle = "glob";
1983
}else{
1984
zMatchStyle = "brlist";
1985
}
1986
}
1987
}
1988
1989
/* Convert r=TAG to t=TAG&rel in order to populate the UI style widgets. */
1990
if( zBrName ){
1991
cgi_delete_query_parameter("r");
1992
cgi_set_query_parameter("t", zBrName); (void)P("t");
1993
cgi_set_query_parameter("rel", "1");
1994
zTagName = zBrName;
1995
if( related==0 ) related = 1;
1996
zType = "ci";
1997
}
1998
1999
/* Ignore empty tag query strings. */
2000
if( zTagName && !*zTagName ){
2001
zTagName = 0;
2002
}
2003
2004
/* Finish preliminary processing of tag match queries. */
2005
matchStyle = match_style(zMatchStyle, MS_EXACT);
2006
if( zTagName ){
2007
zType = "ci";
2008
if( matchStyle==MS_EXACT ){
2009
/* For exact matching, inhibit links to the selected tag. */
2010
zThisTag = zTagName;
2011
Th_StoreUnsafe("current_checkin", zTagName);
2012
}
2013
2014
/* Display a checkbox to enable/disable display of related check-ins. */
2015
if( advancedMenu ){
2016
style_submenu_checkbox("rel", "Related", 0, 0);
2017
}
2018
2019
/* Construct the tag match expression. */
2020
zTagSql = match_tag_sqlexpr(matchStyle, zTagName, &zMatchDesc, &zError);
2021
}
2022
2023
if( zMark && zMark[0]==0 ){
2024
if( zAfter ) zMark = zAfter;
2025
if( zBefore ) zMark = zBefore;
2026
if( zCirca ) zMark = zCirca;
2027
}
2028
if( (zTagSql && db_int(0,"SELECT count(*) "
2029
"FROM tagxref NATURAL JOIN tag WHERE %s",zTagSql/*safe-for-%s*/)<=nEntry)
2030
){
2031
nEntry = -1;
2032
zCirca = 0;
2033
}
2034
if( zType[0]=='a' ){
2035
tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK;
2036
}else{
2037
tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK;
2038
}
2039
if( related ){
2040
tmFlags |= TIMELINE_FILLGAPS | TIMELINE_XMERGE;
2041
tmFlags &= ~TIMELINE_DISJOINT;
2042
}
2043
if( PB("ncp") ){
2044
tmFlags &= ~TIMELINE_CHPICK;
2045
}
2046
if( PB("ng") || zSearch!=0 ){
2047
tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK);
2048
}
2049
if( PB("nsm") ){
2050
style_submenu_enable(0);
2051
}
2052
if( PB("brbg") ){
2053
tmFlags |= TIMELINE_BRCOLOR;
2054
}
2055
if( PB("unhide") ){
2056
tmFlags |= TIMELINE_UNHIDE;
2057
}
2058
if( PB("ubg") ){
2059
tmFlags |= TIMELINE_UCOLOR;
2060
}
2061
if( PB("deltabg") ){
2062
tmFlags |= TIMELINE_DELTA;
2063
}
2064
if( PB("nc") ){
2065
tmFlags &= ~(TIMELINE_DELTA|TIMELINE_BRCOLOR|TIMELINE_UCOLOR);
2066
tmFlags |= TIMELINE_NOCOLOR;
2067
}
2068
if( showSql ) db_append_dml_to_blob(&allSql);
2069
if( zUses!=0 ){
2070
int ufid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUses);
2071
if( ufid ){
2072
if( robot_restrict("timelineX") ) return;
2073
zUses = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ufid);
2074
db_multi_exec("CREATE TEMP TABLE usesfile(rid INTEGER PRIMARY KEY)");
2075
compute_uses_file("usesfile", ufid, 0);
2076
zType = "ci";
2077
disableY = 1;
2078
if( !haveParameterN ) nEntry = 0;
2079
}else{
2080
zUses = 0;
2081
}
2082
}
2083
if( renameOnly ){
2084
db_multi_exec(
2085
"CREATE TEMP TABLE rnfile(rid INTEGER PRIMARY KEY);"
2086
"INSERT OR IGNORE INTO rnfile"
2087
" SELECT mid FROM mlink WHERE pfnid>0 AND pfnid!=fnid;"
2088
);
2089
disableY = 1;
2090
}
2091
if( forkOnly ){
2092
db_multi_exec(
2093
"CREATE TEMP TABLE rnfork(rid INTEGER PRIMARY KEY);\n"
2094
"INSERT OR IGNORE INTO rnfork(rid)\n"
2095
" SELECT pid FROM plink\n"
2096
" WHERE (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n"
2097
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n"
2098
" GROUP BY pid\n"
2099
" HAVING count(*)>1;\n"
2100
"INSERT OR IGNORE INTO rnfork(rid)\n"
2101
" SELECT cid FROM plink\n"
2102
" WHERE (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n"
2103
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n"
2104
" GROUP BY cid\n"
2105
" HAVING count(*)>1;\n",
2106
TAG_BRANCH, TAG_BRANCH, TAG_BRANCH, TAG_BRANCH
2107
);
2108
db_multi_exec(
2109
"INSERT OR IGNORE INTO rnfork(rid)\n"
2110
" SELECT cid FROM plink\n"
2111
" WHERE pid IN rnfork\n"
2112
" AND (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n"
2113
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n"
2114
" UNION\n"
2115
" SELECT pid FROM plink\n"
2116
" WHERE cid IN rnfork\n"
2117
" AND (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n"
2118
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n",
2119
TAG_BRANCH, TAG_BRANCH, TAG_BRANCH, TAG_BRANCH
2120
);
2121
tmFlags |= TIMELINE_UNHIDE;
2122
zType = "ci";
2123
disableY = 1;
2124
}
2125
if( bisectLocal && cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
2126
int iCurrent = db_lget_int("checkout",0);
2127
char *zPerm = bisect_permalink();
2128
bisect_create_bilog_table(iCurrent, 0, 1);
2129
tmFlags |= TIMELINE_UNHIDE | TIMELINE_BISECT | TIMELINE_FILLGAPS;
2130
zType = "ci";
2131
disableY = 1;
2132
style_submenu_element("Permalink", "%R/timeline?bid=%z", zPerm);
2133
}else{
2134
bisectLocal = 0;
2135
}
2136
if( zBisect!=0 && bisect_create_bilog_table(0, zBisect, 1) ){
2137
tmFlags |= TIMELINE_UNHIDE | TIMELINE_BISECT | TIMELINE_FILLGAPS;
2138
zType = "ci";
2139
disableY = 1;
2140
}else{
2141
zBisect = 0;
2142
}
2143
2144
style_header("Timeline");
2145
if( advancedMenu ){
2146
style_submenu_element("Help", "%R/help/www/timeline");
2147
}
2148
login_anonymous_available();
2149
timeline_temp_table();
2150
blob_zero(&sql);
2151
blob_zero(&desc);
2152
blob_append(&sql, "INSERT OR IGNORE INTO timeline ", -1);
2153
blob_append(&sql, timeline_query_for_www(), -1);
2154
if( PB("fc") || PB("v") || PB("detail") ){
2155
tmFlags |= TIMELINE_FCHANGES;
2156
}
2157
if( PB("vfx") ){
2158
tmFlags |= TIMELINE_FORUMTXT;
2159
}
2160
if( (tmFlags & TIMELINE_UNHIDE)==0 ){
2161
blob_append_sql(&sql,
2162
" AND NOT EXISTS(SELECT 1 FROM tagxref"
2163
" WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
2164
TAG_HIDDEN
2165
);
2166
}
2167
if( from_rid && !to_rid && (P("ft")!=0 || P("bt")!=0) ){
2168
const char *zTo = P("ft");
2169
if( zTo ){
2170
from_to_mode = 1;
2171
to_rid = timeline_endpoint(from_rid, zTo, 1);
2172
}else{
2173
from_to_mode = 2;
2174
zTo = P("bt");
2175
to_rid = timeline_endpoint(from_rid, zTo, 0);
2176
}
2177
if( to_rid ){
2178
cgi_replace_parameter("to", zTo);
2179
if( selectedRid==0 ) selectedRid = from_rid;
2180
if( secondaryRid==0 ) secondaryRid = to_rid;
2181
}else{
2182
to_rid = from_rid;
2183
blob_appendf(&desc, "There is no path from %h %s to %h.<br>Instead: ",
2184
P("from"), from_to_mode==1 ? "forward" : "back", zTo);
2185
}
2186
}
2187
if( ((from_rid && to_rid) || (me_rid && you_rid)) && g.perm.Read ){
2188
/* If from= and to= are present, display all nodes on a path connecting
2189
** the two */
2190
PathNode *p = 0;
2191
const char *zFrom = 0;
2192
const char *zTo = 0;
2193
Blob ins;
2194
int nNodeOnPath = 0;
2195
int commonAncs = 0; /* Common ancestors of me_rid and you_rid. */
2196
int earlierRid = 0, laterRid = 0;
2197
int cost = bShort ? 0 : 1;
2198
int nSkip = 0;
2199
2200
if( from_rid && to_rid ){
2201
if( from_to_mode==0 ){
2202
p = path_shortest(from_rid, to_rid, 0, 0, 0, cost);
2203
}else if( from_to_mode==1 ){
2204
p = path_shortest(from_rid, to_rid, 0, 1, 0, cost);
2205
earlierRid = commonAncs = from_rid;
2206
laterRid = to_rid;
2207
}else{
2208
p = path_shortest(to_rid, from_rid, 0, 1, 0, cost);
2209
earlierRid = commonAncs = to_rid;
2210
laterRid = from_rid;
2211
}
2212
zFrom = P("from");
2213
zTo = zTo2 ? zTo2 : P("to");
2214
}else{
2215
commonAncs = path_common_ancestor(me_rid, you_rid);
2216
if( commonAncs!=0 ){
2217
p = path_first();
2218
}
2219
if( commonAncs==you_rid ){
2220
zFrom = P("you");
2221
zTo = P("me");
2222
earlierRid = you_rid;
2223
laterRid = me_rid;
2224
}else{
2225
zFrom = P("me");
2226
zTo = P("you");
2227
earlierRid = me_rid;
2228
laterRid = you_rid;
2229
}
2230
}
2231
blob_init(&ins, 0, 0);
2232
db_multi_exec(
2233
"CREATE TEMP TABLE IF NOT EXISTS pathnode(x INTEGER PRIMARY KEY);"
2234
);
2235
if( p ){
2236
int cnt = 4;
2237
blob_init(&ins, 0, 0);
2238
blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
2239
if( p->u.pTo==0 ) bMin = 0;
2240
for(p=p->u.pTo; p; p=p->u.pTo){
2241
if( bMin
2242
&& p->u.pTo!=0
2243
&& fossil_strcmp(path_branch(p->pFrom),path_branch(p))==0
2244
&& fossil_strcmp(path_branch(p),path_branch(p->u.pTo))==0
2245
){
2246
nSkip++;
2247
}else if( cnt==8 ){
2248
blob_append_sql(&ins, ",\n (%d)", p->rid);
2249
cnt = 0;
2250
}else{
2251
cnt++;
2252
blob_append_sql(&ins, ",(%d)", p->rid);
2253
}
2254
}
2255
}
2256
path_reset();
2257
db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
2258
blob_reset(&ins);
2259
if( related ){
2260
db_multi_exec(
2261
"CREATE TEMP TABLE IF NOT EXISTS related(x INTEGER PRIMARY KEY);"
2262
"INSERT OR IGNORE INTO related(x)"
2263
" SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;"
2264
);
2265
if( related==1 ){
2266
db_multi_exec(
2267
"INSERT OR IGNORE INTO related(x)"
2268
" SELECT cid FROM plink WHERE pid IN pathnode;"
2269
);
2270
}
2271
if( showCherrypicks ){
2272
db_multi_exec(
2273
"INSERT OR IGNORE INTO related(x)"
2274
" SELECT parentid FROM cherrypick WHERE childid IN pathnode;"
2275
);
2276
if( related==1 ){
2277
db_multi_exec(
2278
"INSERT OR IGNORE INTO related(x)"
2279
" SELECT childid FROM cherrypick WHERE parentid IN pathnode;"
2280
);
2281
}
2282
}
2283
if( earlierRid && laterRid && commonAncs==earlierRid ){
2284
/* On a query with me=XXX, you=YYY, and rel, omit all nodes that
2285
** are not ancestors of either XXX or YYY, as those nodes tend to
2286
** be extraneous */
2287
db_multi_exec(
2288
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
2289
);
2290
compute_ancestors(laterRid, 0, 0, earlierRid);
2291
db_multi_exec(
2292
"DELETE FROM related WHERE x NOT IN ok;"
2293
);
2294
}
2295
db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related");
2296
}
2297
add_extra_rids("pathnode",P("x"));
2298
add_extra_rids("pathnode",P("sel1"));
2299
add_extra_rids("pathnode",P("sel2"));
2300
blob_append_sql(&sql, " AND event.objid IN pathnode");
2301
if( zChng ){
2302
db_multi_exec(
2303
"DELETE FROM pathnode\n"
2304
" WHERE NOT EXISTS(SELECT 1 FROM mlink, filename\n"
2305
" WHERE mlink.mid=x\n"
2306
" AND mlink.fnid=filename.fnid\n"
2307
" AND %s)",
2308
glob_expr("filename.name", zChng)
2309
);
2310
}
2311
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
2312
db_multi_exec("%s", blob_sql_text(&sql));
2313
if( advancedMenu ){
2314
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2315
}
2316
nNodeOnPath = db_int(0, "SELECT count(*) FROM temp.pathnode");
2317
if( nNodeOnPath==1 && from_to_mode>0 ){
2318
blob_appendf(&desc,"Check-in ");
2319
}else if( bMin ){
2320
blob_appendf(&desc, "%d of %d check-ins along the path from ",
2321
nNodeOnPath, nNodeOnPath+nSkip);
2322
}else{
2323
blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
2324
}
2325
if( from_rid==selectedRid ){
2326
blob_appendf(&desc, "<span class='timelineSelected'>");
2327
}
2328
blob_appendf(&desc, "%z%h</a>", href("%R/info/%h", zFrom), zFrom);
2329
if( from_rid==selectedRid ) blob_appendf(&desc, "</span>");
2330
if( nNodeOnPath==1 && from_to_mode>0 ){
2331
blob_appendf(&desc, " only");
2332
}else{
2333
blob_append(&desc, " to ", -1);
2334
if( to_rid==secondaryRid ){
2335
blob_appendf(&desc,"<span class='timelineSelected timelineSecondary'>");
2336
}
2337
blob_appendf(&desc, "%z%h</a>", href("%R/info/%h",zTo), zTo);
2338
if( to_rid==secondaryRid ) blob_appendf(&desc, "</span>");
2339
if( related ){
2340
int nRelated = db_int(0, "SELECT count(*) FROM timeline") - nNodeOnPath;
2341
if( nRelated>0 ){
2342
blob_appendf(&desc, " and %d related check-in%s", nRelated,
2343
nRelated>1 ? "s" : "");
2344
}
2345
}
2346
}
2347
addFileGlobDescription(zChng, &desc);
2348
}else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
2349
/* If either p= or d= or both are present, ignore all other parameters
2350
** other than n=, ft=, and bt= */
2351
const char *zBaseName = 0;
2352
int np = 0, nd;
2353
const char *zBackTo = 0;
2354
const char *zFwdTo = 0;
2355
int ridBackTo = 0;
2356
int ridFwdTo = 0;
2357
int bBackAdded = 0; /* True if the zBackTo node was added */
2358
int bFwdAdded = 0; /* True if the zBackTo node was added */
2359
int bSeparateDandP = 0; /* p_rid & d_rid both exist and are distinct */
2360
2361
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
2362
if( p_rid && d_rid && p_rid!=d_rid ){
2363
bSeparateDandP = 1;
2364
db_multi_exec(
2365
"CREATE TEMP TABLE IF NOT EXISTS ok_d(rid INTEGER PRIMARY KEY)"
2366
);
2367
}else{
2368
zBaseName = p_rid ? zDPNameP : zDPNameD;
2369
}
2370
db_multi_exec(
2371
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
2372
);
2373
add_extra_rids("ok", P("x"));
2374
add_extra_rids("ok", P("sel1"));
2375
add_extra_rids("ok", P("sel2"));
2376
blob_append_sql(&sql, " AND event.objid IN ok");
2377
nd = 0;
2378
if( d_rid ){
2379
double rStopTime = 9e99;
2380
zFwdTo = P("ft");
2381
if( zFwdTo && bSeparateDandP ){
2382
if( zError==0 ){
2383
zError = "Cannot use the ft= query parameter when both p= and d= "
2384
"are used and have distinct values.";
2385
}
2386
zFwdTo = 0;
2387
}
2388
if( zFwdTo ){
2389
double rStartDate = mtime_of_rid(d_rid, 0.0);
2390
ridFwdTo = first_checkin_with_tag_after_date(zFwdTo, rStartDate);
2391
if( ridFwdTo==0 ){
2392
ridFwdTo = name_to_typed_rid(zBackTo,"ci");
2393
}
2394
if( ridFwdTo ){
2395
if( !haveParameterN ) nEntry = 0;
2396
rStopTime = mtime_of_rid(ridFwdTo, 9e99);
2397
}
2398
}else if( bSeparateDandP ){
2399
rStopTime = mtime_of_rid(p_rid, 9e99);
2400
nEntry = 0;
2401
}
2402
if( rStopTime<9e99 ){
2403
rStopTime += 5.8e-6; /* Round up by 1/2 second */
2404
}
2405
db_multi_exec(
2406
"WITH RECURSIVE dx(rid,mtime) AS (\n"
2407
" SELECT %d, 0\n"
2408
" UNION\n"
2409
" SELECT plink.cid, plink.mtime FROM dx, plink\n"
2410
" WHERE plink.pid=dx.rid\n"
2411
" AND plink.mtime<=%.*g\n"
2412
" ORDER BY 2\n"
2413
")\n"
2414
"INSERT OR IGNORE INTO ok SELECT rid FROM dx LIMIT %d",
2415
d_rid, rStopTime<8e99 ? 17 : 2, rStopTime, nEntry<=0 ? -1 : nEntry+1
2416
);
2417
if( ridFwdTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridFwdTo) ){
2418
db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridFwdTo);
2419
bFwdAdded = 1;
2420
}
2421
if( bSeparateDandP ){
2422
db_multi_exec(
2423
"INSERT INTO ok_d SELECT rid FROM ok;"
2424
"DELETE FROM ok;"
2425
);
2426
}else{
2427
removeFileGlobFromOk(zChng);
2428
nd = db_int(0, "SELECT count(*)-1 FROM ok");
2429
if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
2430
if( nd>0 || p_rid==0 ){
2431
blob_appendf(&desc, "%d descendant%s",
2432
nd>=0 ? nd : 0,(1==nd)?"":"s");
2433
}
2434
if( useDividers && !selectedRid ) selectedRid = d_rid;
2435
db_multi_exec("DELETE FROM ok");
2436
}
2437
}
2438
if( p_rid ){
2439
zBackTo = P("bt");
2440
if( zBackTo && bSeparateDandP ){
2441
if( zError==0 ){
2442
zError = "Cannot use the bt= query parameter when both p= and d= "
2443
"are used and have distinct values.";
2444
}
2445
zBackTo = 0;
2446
}
2447
if( zBackTo ){
2448
double rDateLimit = mtime_of_rid(p_rid, 0.0);
2449
ridBackTo = last_checkin_with_tag_before_date(zBackTo, rDateLimit);
2450
if( ridBackTo==0 ){
2451
ridBackTo = name_to_typed_rid(zBackTo,"ci");
2452
}
2453
if( ridBackTo && !haveParameterN ) nEntry = 0;
2454
}else if( bSeparateDandP ){
2455
ridBackTo = d_rid;
2456
nEntry = 0;
2457
}
2458
compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0, ridBackTo);
2459
if( ridBackTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridBackTo) ){
2460
db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridBackTo);
2461
bBackAdded = 1;
2462
}
2463
if( bSeparateDandP ){
2464
db_multi_exec("DELETE FROM ok WHERE rid NOT IN ok_d;");
2465
removeFileGlobFromOk(zChng);
2466
db_multi_exec("%s", blob_sql_text(&sql));
2467
}else{
2468
removeFileGlobFromOk(zChng);
2469
np = db_int(0, "SELECT count(*)-1 FROM ok");
2470
if( np>0 || nd==0 ){
2471
if( nd>0 ) blob_appendf(&desc, " and ");
2472
blob_appendf(&desc, "%d ancestor%s",
2473
np>=0 ? np : 0, (1==np)?"":"s");
2474
db_multi_exec("%s", blob_sql_text(&sql));
2475
}
2476
if( useDividers && !selectedRid ) selectedRid = p_rid;
2477
}
2478
}
2479
2480
if( bSeparateDandP ){
2481
int n = db_int(0, "SELECT count(*) FROM ok");
2482
blob_reset(&desc);
2483
blob_appendf(&desc,
2484
"%d check-ins that are derived from %z%h</a>"
2485
" and contribute to %z%h</a>",
2486
n,
2487
href("%R/info?name=%h",zDPNameD),zDPNameD,
2488
href("%R/info?name=%h",zDPNameP),zDPNameP
2489
);
2490
ridBackTo = 0;
2491
ridFwdTo = 0;
2492
}else{
2493
blob_appendf(&desc, " of %z%h</a>",
2494
href("%R/info?name=%h", zBaseName), zBaseName);
2495
}
2496
if( ridBackTo ){
2497
if( np==0 ){
2498
blob_reset(&desc);
2499
blob_appendf(&desc,
2500
"Check-in %z%h</a> only (%z%h</a> does not precede it)",
2501
href("%R/info?name=%h",zBaseName), zBaseName,
2502
href("%R/info?name=%h",zBackTo), zBackTo);
2503
}else{
2504
blob_appendf(&desc, " back to %z%h</a>%s",
2505
href("%R/info?name=%h",zBackTo), zBackTo,
2506
bBackAdded ? " (not a direct ancestor)" : "");
2507
if( ridFwdTo && zFwdTo ){
2508
blob_appendf(&desc, " and up to %z%h</a>%s",
2509
href("%R/info?name=%h",zFwdTo), zFwdTo,
2510
bFwdAdded ? " (not a direct descendant)" : "");
2511
}
2512
}
2513
}else if( ridFwdTo ){
2514
if( nd==0 ){
2515
blob_reset(&desc);
2516
blob_appendf(&desc,
2517
"Check-in %z%h</a> only (%z%h</a> does not follow it)",
2518
href("%R/info?name=%h",zBaseName), zBaseName,
2519
href("%R/info?name=%h",zFwdTo), zFwdTo);
2520
}else{
2521
blob_appendf(&desc, " up to %z%h</a>%s",
2522
href("%R/info?name=%h",zFwdTo), zFwdTo,
2523
bFwdAdded ? " (not a direct descendant)":"");
2524
}
2525
}
2526
if( zChng ){
2527
if( strstr(blob_str(&desc)," that ") ) blob_appendf(&desc, " and");
2528
blob_appendf(&desc, " that make changes to files matching \"%h\"", zChng);
2529
}
2530
if( advancedMenu ){
2531
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2532
}
2533
style_submenu_entry("n","Max:",4,0);
2534
timeline_y_submenu(1);
2535
}else if( f_rid && g.perm.Read ){
2536
/* If f= is present, ignore all other parameters other than n= */
2537
char *zUuid;
2538
db_multi_exec(
2539
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);"
2540
"INSERT INTO ok VALUES(%d);"
2541
"INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
2542
"INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
2543
f_rid, f_rid, f_rid
2544
);
2545
if( showCherrypicks ){
2546
db_multi_exec(
2547
"INSERT OR IGNORE INTO ok SELECT parentid FROM cherrypick"
2548
" WHERE childid=%d;"
2549
"INSERT OR IGNORE INTO ok SELECT childid FROM cherrypick"
2550
" WHERE parentid=%d;",
2551
f_rid, f_rid
2552
);
2553
}
2554
blob_append_sql(&sql, " AND event.objid IN ok");
2555
db_multi_exec("%s", blob_sql_text(&sql));
2556
if( useDividers && !selectedRid ) selectedRid = f_rid;
2557
blob_appendf(&desc, "Parents and children of check-in ");
2558
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
2559
blob_appendf(&desc, "%z[%S]</a>", href("%R/info/%!S", zUuid), zUuid);
2560
tmFlags |= TIMELINE_XMERGE;
2561
if( advancedMenu ){
2562
style_submenu_checkbox("unhide", "Unhide", 0, 0);
2563
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2564
}
2565
}else{
2566
/* Otherwise, a timeline based on a span of time */
2567
int n;
2568
const char *zEType = "event";
2569
char *zDate;
2570
Blob cond;
2571
blob_zero(&cond);
2572
tmFlags |= TIMELINE_FILLGAPS;
2573
if( zChng && *zChng ){
2574
addFileGlobExclusion(zChng, &cond);
2575
tmFlags |= TIMELINE_XMERGE;
2576
}
2577
if( zUses ){
2578
blob_append_sql(&cond, " AND event.objid IN usesfile\n");
2579
}
2580
if( renameOnly ){
2581
blob_append_sql(&cond, " AND event.objid IN rnfile\n");
2582
}
2583
if( forkOnly ){
2584
blob_append_sql(&cond, " AND event.objid IN rnfork\n");
2585
}
2586
if( cpOnly && showCherrypicks ){
2587
db_multi_exec(
2588
"CREATE TEMP TABLE IF NOT EXISTS cpnodes(rid INTEGER PRIMARY KEY);"
2589
"INSERT OR IGNORE INTO cpnodes SELECT childid FROM cherrypick;"
2590
"INSERT OR IGNORE INTO cpnodes SELECT parentid FROM cherrypick;"
2591
);
2592
blob_append_sql(&cond, " AND event.objid IN cpnodes\n");
2593
}
2594
if( bisectLocal || zBisect!=0 ){
2595
blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog)\n");
2596
}
2597
if( zYearMonth ){
2598
char *zNext;
2599
int bZulu = 0;
2600
const char *zTZMod;
2601
zYearMonth = timeline_expand_datetime(zYearMonth, &bZulu);
2602
zYearMonth = mprintf("%.7s", zYearMonth);
2603
if( db_int(0,"SELECT julianday('%q-01') IS NULL", zYearMonth) ){
2604
zYearMonth = db_text(0, "SELECT strftime('%%Y-%%m','now');");
2605
}
2606
zTZMod = (bZulu==0 && fossil_ui_localtime()) ? "utc" : "+00:00";
2607
if( db_int(0,
2608
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2609
" WHERE blob.rid=event.objid"
2610
" AND mtime>=julianday('%q-01',%Q)%s)",
2611
zYearMonth, zTZMod, blob_sql_text(&cond))
2612
){
2613
zNext = db_text(0, "SELECT strftime('%%Y%%m%q','%q-01','+1 month');",
2614
&"Z"[!bZulu], zYearMonth);
2615
zNewerButton = fossil_strdup(url_render(&url, "ym", zNext, 0, 0));
2616
zNewerButtonLabel = "Following month";
2617
fossil_free(zNext);
2618
}
2619
if( db_int(0,
2620
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2621
" WHERE blob.rid=event.objid"
2622
" AND mtime<julianday('%q-01',%Q)%s)",
2623
zYearMonth, zTZMod, blob_sql_text(&cond))
2624
){
2625
zNext = db_text(0, "SELECT strftime('%%Y%%m%q','%q-01','-1 month');",
2626
&"Z"[!bZulu], zYearMonth);
2627
zOlderButton = fossil_strdup(url_render(&url, "ym", zNext, 0, 0));
2628
zOlderButtonLabel = "Previous month";
2629
fossil_free(zNext);
2630
}
2631
blob_append_sql(&cond,
2632
" AND event.mtime>=julianday('%q-01',%Q)"
2633
" AND event.mtime<julianday('%q-01',%Q,'+1 month')\n",
2634
zYearMonth, zTZMod, zYearMonth, zTZMod);
2635
nEntry = -1;
2636
/* Adjust the zYearMonth for the title */
2637
zYearMonth = mprintf("%z-01%s", zYearMonth, &"Z"[!bZulu]);
2638
}
2639
else if( zYearWeek ){
2640
char *z, *zNext;
2641
int bZulu = 0;
2642
const char *zTZMod;
2643
zYearWeek = timeline_expand_datetime(zYearWeek, &bZulu);
2644
z = db_text(0, "SELECT strftime('%%Y-%%W',%Q)", zYearWeek);
2645
if( z && z[0] ){
2646
zYearWeekStart = db_text(0, "SELECT date(%Q,'-6 days','weekday 1')",
2647
zYearWeek);
2648
zYearWeek = z;
2649
}else{
2650
if( strlen(zYearWeek)==7 ){
2651
zYearWeekStart = db_text(0,
2652
"SELECT date('%.4q-01-01','%+d days','weekday 1')",
2653
zYearWeek, atoi(zYearWeek+5)*7-6);
2654
}else{
2655
zYearWeekStart = 0;
2656
}
2657
if( zYearWeekStart==0 || zYearWeekStart[0]==0 ){
2658
zYearWeekStart = db_text(0,
2659
"SELECT date('now','-6 days','weekday 1');");
2660
zYearWeek = db_text(0,
2661
"SELECT strftime('%%Y-%%W','now','-6 days','weekday 1')");
2662
}
2663
}
2664
zTZMod = (bZulu==0 && fossil_ui_localtime()) ? "utc" : "+00:00";
2665
if( db_int(0,
2666
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2667
" WHERE blob.rid=event.objid"
2668
" AND mtime>=julianday(%Q,%Q)%s)",
2669
zYearWeekStart, zTZMod, blob_sql_text(&cond))
2670
){
2671
zNext = db_text(0, "SELECT strftime('%%Y%%W%q',%Q,'+7 day');",
2672
&"Z"[!bZulu], zYearWeekStart);
2673
zNewerButton = fossil_strdup(url_render(&url, "yw", zNext, 0, 0));
2674
zNewerButtonLabel = "Following week";
2675
fossil_free(zNext);
2676
}
2677
if( db_int(0,
2678
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2679
" WHERE blob.rid=event.objid"
2680
" AND mtime<julianday(%Q,%Q)%s)",
2681
zYearWeekStart, zTZMod, blob_sql_text(&cond))
2682
){
2683
zNext = db_text(0, "SELECT strftime('%%Y%%W%q',%Q,'-7 days');",
2684
&"Z"[!bZulu], zYearWeekStart);
2685
zOlderButton = fossil_strdup(url_render(&url, "yw", zNext, 0, 0));
2686
zOlderButtonLabel = "Previous week";
2687
fossil_free(zNext);
2688
}
2689
blob_append_sql(&cond,
2690
" AND event.mtime>=julianday(%Q,%Q)"
2691
" AND event.mtime<julianday(%Q,%Q,'+7 days')\n",
2692
zYearWeekStart, zTZMod, zYearWeekStart, zTZMod);
2693
nEntry = -1;
2694
if( fossil_ui_localtime() && bZulu ){
2695
zYearWeekStart = mprintf("%zZ", zYearWeekStart);
2696
}
2697
}
2698
else if( zDay && timeline_is_datespan(zDay) ){
2699
char *zNext;
2700
char *zStart, *zEnd;
2701
int nDay;
2702
int bZulu = 0;
2703
const char *zTZMod;
2704
zEnd = db_text(0, "SELECT date(%Q)",
2705
timeline_expand_datetime(zDay+9, &bZulu));
2706
zStart = db_text(0, "SELECT date('%.4q-%.2q-%.2q')",
2707
zDay, zDay+4, zDay+6);
2708
nDay = db_int(0, "SELECT julianday(%Q)-julianday(%Q)", zEnd, zStart);
2709
if( nDay==0 ){
2710
zDay = &zDay[9];
2711
goto single_ymd;
2712
}
2713
if( nDay<0 ){
2714
char *zTemp = zEnd;
2715
zEnd = zStart;
2716
zStart = zTemp;
2717
nDay = 1 - nDay;
2718
}else{
2719
nDay += 1;
2720
}
2721
zTZMod = (bZulu==0 && fossil_ui_localtime()) ? "utc" : "+00:00";
2722
if( nDay>0 && db_int(0,
2723
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2724
" WHERE blob.rid=event.objid"
2725
" AND mtime>=julianday(%Q,'1 day',%Q)%s)",
2726
zEnd, zTZMod, blob_sql_text(&cond))
2727
){
2728
zNext = db_text(0,
2729
"SELECT strftime('%%Y%%m%%d-',%Q,'%d days')||"
2730
"strftime('%%Y%%m%%d%q',%Q,'%d day');",
2731
zStart, nDay, &"Z"[!bZulu], zEnd, nDay);
2732
zNewerButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0));
2733
zNewerButtonLabel = mprintf("Following %d days", nDay);
2734
fossil_free(zNext);
2735
}
2736
if( nDay>1 && db_int(0,
2737
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2738
" WHERE blob.rid=event.objid"
2739
" AND mtime<julianday(%Q,'-1 day',%Q)%s)",
2740
zStart, zTZMod, blob_sql_text(&cond))
2741
){
2742
zNext = db_text(0,
2743
"SELECT strftime('%%Y%%m%%d-',%Q,'%d days')||"
2744
"strftime('%%Y%%m%%d%q',%Q,'%d day');",
2745
zStart, -nDay, &"Z"[!bZulu], zEnd, -nDay);
2746
zOlderButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0));
2747
zOlderButtonLabel = mprintf("Previous %d days", nDay);
2748
fossil_free(zNext);
2749
}
2750
blob_append_sql(&cond,
2751
" AND event.mtime>=julianday(%Q,%Q)"
2752
" AND event.mtime<julianday(%Q,%Q,'+1 day')\n",
2753
zStart, zTZMod, zEnd, zTZMod);
2754
nEntry = -1;
2755
2756
if( fossil_ui_localtime() && bZulu ){
2757
zDay = mprintf("%d days between %zZ and %zZ", nDay, zStart, zEnd);
2758
}else{
2759
zDay = mprintf("%d days between %z and %z", nDay, zStart, zEnd);
2760
}
2761
}
2762
else if( zDay ){
2763
char *zNext;
2764
int bZulu = 0;
2765
const char *zTZMod;
2766
single_ymd:
2767
bZulu = 0;
2768
zDay = timeline_expand_datetime(zDay, &bZulu);
2769
zDay = db_text(0, "SELECT date(%Q)", zDay);
2770
if( zDay==0 || zDay[0]==0 ){
2771
zDay = db_text(0, "SELECT date('now')");
2772
}
2773
zTZMod = (bZulu==0 && fossil_ui_localtime()) ? "utc" : "+00:00";
2774
if( db_int(0,
2775
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2776
" WHERE blob.rid=event.objid"
2777
" AND mtime>=julianday(%Q,'+1 day',%Q)%s)",
2778
zDay, zTZMod, blob_sql_text(&cond))
2779
){
2780
zNext = db_text(0,"SELECT strftime('%%Y%%m%%d%q',%Q,'+1 day');",
2781
&"Z"[!bZulu], zDay);
2782
zNewerButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0));
2783
zNewerButtonLabel = "Following day";
2784
fossil_free(zNext);
2785
}
2786
if( db_int(0,
2787
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2788
" WHERE blob.rid=event.objid"
2789
" AND mtime<julianday(%Q,'-1 day',%Q)%s)",
2790
zDay, zTZMod, blob_sql_text(&cond))
2791
){
2792
zNext = db_text(0,"SELECT strftime('%%Y%%m%%d%q',%Q,'-1 day');",
2793
&"Z"[!bZulu], zDay);
2794
zOlderButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0));
2795
zOlderButtonLabel = "Previous day";
2796
fossil_free(zNext);
2797
}
2798
blob_append_sql(&cond,
2799
" AND event.mtime>=julianday(%Q,%Q)"
2800
" AND event.mtime<julianday(%Q,%Q,'+1 day')\n",
2801
zDay, zTZMod, zDay, zTZMod);
2802
nEntry = -1;
2803
if( fossil_ui_localtime() && bZulu ){
2804
zDay = mprintf("%zZ", zDay); /* Add Z suffix to day for the title */
2805
}
2806
}
2807
else if( zNDays ){
2808
nDays = atoi(zNDays);
2809
if( nDays<1 ) nDays = 1;
2810
blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
2811
nDays);
2812
nEntry = -1;
2813
}
2814
else if( zYear &&
2815
((4==strlen(zYear) && atoi(zYear)>1900)
2816
|| (1==strlen(zYear) && 0==atoi(zYear)))){
2817
int year = atoi(zYear);
2818
char *zNext = 0;
2819
if(0==year){/*use current year*/
2820
Stmt qy;
2821
db_prepare(&qy, "SELECT strftime('%%Y','now')");
2822
db_step(&qy);
2823
year = db_column_int(&qy, 0);
2824
zYear = fossil_strdup(db_column_text(&qy, 0));
2825
db_finalize(&qy);
2826
}else{
2827
zNext = mprintf("%d", year+1);
2828
if( db_int(0,
2829
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2830
" WHERE blob.rid=event.objid AND strftime('%%Y',mtime)=%Q %s)",
2831
zNext, blob_sql_text(&cond))
2832
){
2833
zNewerButton = fossil_strdup(url_render(&url, "year", zNext, 0, 0));
2834
zNewerButtonLabel = "Following year";
2835
}
2836
fossil_free(zNext);
2837
}
2838
zNext = mprintf("%d", year-1);
2839
if( db_int(0,
2840
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
2841
" WHERE blob.rid=event.objid AND strftime('%%Y',mtime)=%Q %s)",
2842
zNext, blob_sql_text(&cond))
2843
){
2844
zOlderButton = fossil_strdup(url_render(&url, "year", zNext, 0, 0));
2845
zOlderButtonLabel = "Previous year";
2846
}
2847
fossil_free(zNext);
2848
blob_append_sql(&cond, " AND %Q=strftime('%%Y',event.mtime) ",
2849
zYear);
2850
nEntry = -1;
2851
}
2852
if( zTagSql ){
2853
db_multi_exec(
2854
"CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);"
2855
"INSERT OR IGNORE INTO selected_nodes\n"
2856
" SELECT tagxref.rid FROM tagxref NATURAL JOIN tag\n"
2857
" WHERE tagtype>0\n"
2858
" AND %s", zTagSql/*safe-for-%s*/
2859
);
2860
if( zMark ){
2861
/* If the t=release option is used with m=UUID, then also
2862
** include the UUID check-in in the display list */
2863
int ridMark = name_to_rid(zMark);
2864
db_multi_exec(
2865
"INSERT OR IGNORE INTO selected_nodes(rid) VALUES(%d)", ridMark);
2866
}
2867
add_extra_rids("selected_nodes",P("x"));
2868
add_extra_rids("selected_nodes",P("sel1"));
2869
add_extra_rids("selected_nodes",P("sel2"));
2870
if( related==0 ){
2871
blob_append_sql(&cond, " AND blob.rid IN selected_nodes");
2872
}else{
2873
db_multi_exec(
2874
"CREATE TEMP TABLE related_nodes(rid INTEGER PRIMARY KEY);"
2875
"INSERT INTO related_nodes SELECT rid FROM selected_nodes;"
2876
);
2877
blob_append_sql(&cond, " AND blob.rid IN related_nodes");
2878
/* The next two blob_appendf() calls add SQL that causes check-ins that
2879
** are not part of the branch which are parents or children of the
2880
** branch to be included in the report. These related check-ins are
2881
** useful in helping to visualize what has happened on a quiescent
2882
** branch that is infrequently merged with a much more activate branch.
2883
*/
2884
db_multi_exec(
2885
"INSERT OR IGNORE INTO related_nodes\n"
2886
" SELECT pid FROM selected_nodes CROSS JOIN plink\n"
2887
" WHERE selected_nodes.rid=plink.cid;"
2888
);
2889
if( related==1 ){
2890
db_multi_exec(
2891
"INSERT OR IGNORE INTO related_nodes\n"
2892
" SELECT cid FROM selected_nodes CROSS JOIN plink\n"
2893
" WHERE selected_nodes.rid=plink.pid;"
2894
);
2895
if( showCherrypicks ){
2896
db_multi_exec(
2897
"INSERT OR IGNORE INTO related_nodes\n"
2898
" SELECT childid FROM selected_nodes CROSS JOIN cherrypick\n"
2899
" WHERE selected_nodes.rid=cherrypick.parentid;"
2900
);
2901
}
2902
}
2903
if( showCherrypicks ){
2904
db_multi_exec(
2905
"INSERT OR IGNORE INTO related_nodes\n"
2906
" SELECT parentid FROM selected_nodes CROSS JOIN cherrypick\n"
2907
" WHERE selected_nodes.rid=cherrypick.childid;"
2908
);
2909
}
2910
if( (tmFlags & TIMELINE_UNHIDE)==0 ){
2911
db_multi_exec(
2912
"DELETE FROM related_nodes\n"
2913
" WHERE rid IN (SELECT related_nodes.rid\n"
2914
" FROM related_nodes, tagxref\n"
2915
" WHERE tagid=%d AND tagtype>0\n"
2916
" AND tagxref.rid=related_nodes.rid)",
2917
TAG_HIDDEN
2918
);
2919
}
2920
}
2921
}
2922
if( (zType[0]=='w' && !g.perm.RdWiki)
2923
|| (zType[0]=='t' && !g.perm.RdTkt)
2924
|| (zType[0]=='n' && !g.perm.RdTkt)
2925
|| (zType[0]=='e' && !g.perm.RdWiki)
2926
|| (zType[0]=='c' && !g.perm.Read)
2927
|| (zType[0]=='g' && !g.perm.Read)
2928
|| (zType[0]=='f' && !g.perm.RdForum)
2929
){
2930
zType = "all";
2931
}
2932
if( zType[0]=='a' ){
2933
if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){
2934
char cSep = '(';
2935
blob_append_sql(&cond, " AND event.type IN ");
2936
if( g.perm.Read ){
2937
blob_append_sql(&cond, "%c'ci','g'", cSep);
2938
cSep = ',';
2939
}
2940
if( g.perm.RdWiki ){
2941
blob_append_sql(&cond, "%c'w','e'", cSep);
2942
cSep = ',';
2943
}
2944
if( g.perm.RdTkt ){
2945
blob_append_sql(&cond, "%c't'", cSep);
2946
cSep = ',';
2947
}
2948
if( g.perm.RdForum ){
2949
blob_append_sql(&cond, "%c'f'", cSep);
2950
cSep = ',';
2951
}
2952
blob_append_sql(&cond, ")");
2953
}
2954
}else{ /* zType!="all" */
2955
if( zType[0]=='n' ){
2956
blob_append_sql(&cond,
2957
" AND event.type='t' AND event.comment GLOB 'New ticket*'");
2958
}else{
2959
blob_append_sql(&cond, " AND event.type=%Q", zType);
2960
}
2961
if( zType[0]=='c' ){
2962
zEType = "check-in";
2963
}else if( zType[0]=='w' ){
2964
zEType = "wiki";
2965
}else if( zType[0]=='t' ){
2966
zEType = "ticket change";
2967
}else if( zType[0]=='n' ){
2968
zEType = "new ticket";
2969
}else if( zType[0]=='e' ){
2970
zEType = "technical note";
2971
}else if( zType[0]=='g' ){
2972
zEType = "tag";
2973
}else if( zType[0]=='f' ){
2974
zEType = "forum post";
2975
}
2976
}
2977
if( zUser ){
2978
int n = db_int(0,"SELECT count(*) FROM event"
2979
" WHERE user=%Q OR euser=%Q", zUser, zUser);
2980
if( n<=nEntry ){
2981
nEntry = -1;
2982
}
2983
blob_append_sql(&cond, " AND (event.user=%Q OR event.euser=%Q)",
2984
zUser, zUser);
2985
zThisUser = zUser;
2986
}
2987
if( zSearch ){
2988
if( tmFlags & TIMELINE_FORUMTXT ){
2989
sqlite3_create_function(g.db, "forum_post_content", 1, SQLITE_UTF8,
2990
0, forum_post_content_function, 0, 0);
2991
blob_append_sql(&cond,
2992
" AND (event.comment LIKE '%%%q%%'"
2993
" OR event.brief LIKE '%%%q%%'"
2994
" OR (event.type=='f' AND"
2995
" forum_post_content(event.objid) LIKE '%%%q%%'))",
2996
zSearch, zSearch, zSearch);
2997
}else{
2998
blob_append_sql(&cond,
2999
" AND (event.comment LIKE '%%%q%%' OR event.brief LIKE '%%%q%%')",
3000
zSearch, zSearch);
3001
}
3002
}
3003
rBefore = symbolic_name_to_mtime(zBefore, &zBefore, 1);
3004
rAfter = symbolic_name_to_mtime(zAfter, &zAfter, 0);
3005
rCirca = symbolic_name_to_mtime(zCirca, &zCirca, 0);
3006
blob_append_sql(&sql, "%s", blob_sql_text(&cond));
3007
if( rAfter>0.0 ){
3008
if( rBefore>0.0 ){
3009
blob_append_sql(&sql,
3010
" AND event.mtime>=%.17g AND event.mtime<=%.17g\n"
3011
" ORDER BY event.mtime ASC", rAfter-ONE_SECOND, rBefore+ONE_SECOND);
3012
nEntry = -1;
3013
}else{
3014
blob_append_sql(&sql,
3015
" AND event.mtime>=%.17g\n ORDER BY event.mtime ASC",
3016
rAfter-ONE_SECOND);
3017
}
3018
zCirca = 0;
3019
url_add_parameter(&url, "c", 0);
3020
}else if( rBefore>0.0 ){
3021
blob_append_sql(&sql,
3022
" AND event.mtime<=%.17g\n ORDER BY event.mtime DESC",
3023
rBefore+ONE_SECOND);
3024
zCirca = 0;
3025
url_add_parameter(&url, "c", 0);
3026
}else if( rCirca>0.0 ){
3027
Blob sql2;
3028
blob_init(&sql2, blob_sql_text(&sql), -1);
3029
blob_append_sql(&sql2,
3030
" AND event.mtime>=%f\n ORDER BY event.mtime ASC", rCirca);
3031
if( nEntry>0 ){
3032
blob_append_sql(&sql2," LIMIT %d", (nEntry+1)/2);
3033
}
3034
db_multi_exec("%s", blob_sql_text(&sql2));
3035
if( nEntry>0 ){
3036
nEntry -= db_int(0,"select count(*) from timeline");
3037
if( nEntry<=0 ) nEntry = 1;
3038
}
3039
blob_reset(&sql2);
3040
blob_append_sql(&sql,
3041
" AND event.mtime<=%f\n ORDER BY event.mtime DESC",
3042
rCirca
3043
);
3044
if( zMark==0 ) zMark = zCirca;
3045
}else{
3046
blob_append_sql(&sql, " ORDER BY event.mtime DESC");
3047
}
3048
if( nEntry>0 ) blob_append_sql(&sql, " LIMIT %d", nEntry);
3049
db_multi_exec("%s", blob_sql_text(&sql));
3050
3051
n = db_int(0, "SELECT count(*) FROM timeline WHERE etype!='div' /*scan*/");
3052
zPlural = n==1 ? "" : "s";
3053
if( zYearMonth ){
3054
blob_appendf(&desc, "%d %s%s for the month beginning %h",
3055
n, zEType, zPlural, zYearMonth);
3056
}else if( zYearWeek ){
3057
blob_appendf(&desc, "%d %s%s for week %h beginning on %h",
3058
n, zEType, zPlural, zYearWeek, zYearWeekStart);
3059
}else if( zDay ){
3060
blob_appendf(&desc, "%d %s%s occurring on %h", n, zEType, zPlural, zDay);
3061
}else if( zNDays ){
3062
blob_appendf(&desc, "%d %s%s within the past %d day%s",
3063
n, zEType, zPlural, nDays, nDays>1 ? "s" : "");
3064
}else if( zBefore==0 && zCirca==0 && n>=nEntry && nEntry>0 ){
3065
blob_appendf(&desc, "%d most recent %s%s", n, zEType, zPlural);
3066
}else{
3067
blob_appendf(&desc, "%d %s%s", n, zEType, zPlural);
3068
}
3069
if( zUses ){
3070
char *zFilenames = names_of_file(zUses);
3071
blob_appendf(&desc, " using file %s version %z%S</a>", zFilenames,
3072
href("%R/artifact/%!S",zUses), zUses);
3073
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
3074
}
3075
if( renameOnly ){
3076
blob_appendf(&desc, " that contain filename changes");
3077
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
3078
}
3079
if( forkOnly ){
3080
blob_appendf(&desc, " associated with forks");
3081
tmFlags |= TIMELINE_DISJOINT;
3082
}
3083
if( bisectLocal || zBisect!=0 ){
3084
blob_appendf(&desc, " in a bisect");
3085
tmFlags |= TIMELINE_DISJOINT;
3086
}
3087
if( cpOnly && showCherrypicks ){
3088
blob_appendf(&desc, " that participate in a cherrypick merge");
3089
tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT;
3090
}
3091
if( zUser ){
3092
if( g.perm.Admin ){
3093
blob_appendf(&desc, " by user <a href='%R/setup_uinfo?l=%h'>%h</a>",
3094
zUser, zUser);
3095
}else{
3096
blob_appendf(&desc, " by user %h", zUser);
3097
}
3098
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
3099
}
3100
if( zTagSql ){
3101
if( matchStyle==MS_EXACT || matchStyle==MS_BRLIST ){
3102
if( related ){
3103
blob_appendf(&desc, " related to %h", zMatchDesc);
3104
}else{
3105
blob_appendf(&desc, " tagged with %h", zMatchDesc);
3106
}
3107
}else{
3108
if( related ){
3109
blob_appendf(&desc, " related to tags matching %h", zMatchDesc);
3110
}else{
3111
blob_appendf(&desc, " with tags matching %h", zMatchDesc);
3112
}
3113
}
3114
if( zMark ){
3115
blob_appendf(&desc," plus check-in \"%h\"", zMark);
3116
}
3117
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
3118
}
3119
addFileGlobDescription(zChng, &desc);
3120
if( rAfter>0.0 ){
3121
if( rBefore>0.0 ){
3122
blob_appendf(&desc, " occurring between %h and %h.<br>",
3123
zAfter, zBefore);
3124
}else{
3125
blob_appendf(&desc, " occurring on or after %h.<br>", zAfter);
3126
}
3127
}else if( rBefore>0.0 ){
3128
blob_appendf(&desc, " occurring on or before %h.<br>", zBefore);
3129
}else if( rCirca>0.0 ){
3130
blob_appendf(&desc, " occurring around %h.<br>", zCirca);
3131
}
3132
if( zSearch ){
3133
blob_appendf(&desc, " matching \"%h\"", zSearch);
3134
}
3135
if( g.perm.Hyperlink ){
3136
static const char *const azMatchStyles[] = {
3137
"exact", "Exact", "glob", "Glob", "like", "Like", "regexp", "Regexp",
3138
"brlist", "List"
3139
};
3140
double rDate;
3141
zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/");
3142
if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){
3143
zDate = fossil_strdup((zAfter ? zAfter : zBefore));
3144
}
3145
if( zDate ){
3146
rDate = symbolic_name_to_mtime(zDate, 0, 0);
3147
if( db_int(0,
3148
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
3149
" WHERE blob.rid=event.objid AND mtime<=%.17g%s)",
3150
rDate-ONE_SECOND, blob_sql_text(&cond))
3151
){
3152
zOlderButton = fossil_strdup(url_render(&url, "b", zDate, "a", 0));
3153
zOlderButtonLabel = "More";
3154
}
3155
free(zDate);
3156
}
3157
zDate = db_text(0, "SELECT max(timestamp) FROM timeline /*scan*/");
3158
if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){
3159
zDate = fossil_strdup((zBefore ? zBefore : zAfter));
3160
}
3161
if( zDate ){
3162
rDate = symbolic_name_to_mtime(zDate, 0, 0);
3163
if( db_int(0,
3164
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
3165
" WHERE blob.rid=event.objid AND mtime>=%.17g%s)",
3166
rDate+ONE_SECOND, blob_sql_text(&cond))
3167
){
3168
zNewerButton = fossil_strdup(url_render(&url, "a", zDate, "b", 0));
3169
zNewerButtonLabel = "More";
3170
}
3171
free(zDate);
3172
}
3173
if( advancedMenu ){
3174
if( zType[0]=='a' || zType[0]=='c' ){
3175
style_submenu_checkbox("unhide", "Unhide", 0, 0);
3176
}
3177
style_submenu_checkbox("v", "Files",(zType[0]!='a' && zType[0]!='c'),0);
3178
}
3179
style_submenu_entry("n","Max:",4,0);
3180
timeline_y_submenu(disableY);
3181
if( advancedMenu ){
3182
style_submenu_entry("t", "Tag Filter:", -8, 0);
3183
style_submenu_multichoice("ms", count(azMatchStyles)/2,azMatchStyles,0);
3184
}
3185
}
3186
blob_zero(&cond);
3187
}
3188
if( showSql ){
3189
db_append_dml_to_blob(0);
3190
@ <pre>%h(blob_str(&allSql))</pre>
3191
blob_reset(&allSql);
3192
}
3193
if( search_restrict(SRCH_CKIN)!=0 ){
3194
style_submenu_element("Search", "%R/search?y=c");
3195
}
3196
if( advancedMenu ){
3197
style_submenu_element("Basic", "%s",
3198
url_render(&url, "advm", "0", "udc", "1"));
3199
}else{
3200
style_submenu_element("Advanced", "%s",
3201
url_render(&url, "advm", "1", "udc", "1"));
3202
}
3203
if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
3204
if( useDividers && zMark && zMark[0] ){
3205
double r = symbolic_name_to_mtime(zMark, 0, 0);
3206
if( r>0.0 && !selectedRid ) selectedRid = timeline_add_divider(r);
3207
}
3208
blob_zero(&sql);
3209
if( PB("oldestfirst") ){
3210
db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby ASC /*scan*/");
3211
tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK);
3212
}else{
3213
db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
3214
}
3215
if( fossil_islower(desc.aData[0]) ){
3216
desc.aData[0] = fossil_toupper(desc.aData[0]);
3217
}
3218
if( zBrName ){
3219
if( !PB("nowiki")
3220
&& wiki_render_associated("branch", zBrName, WIKIASSOC_ALL)
3221
){
3222
@ <div class="section">%b(&desc)</div>
3223
} else{
3224
@ <h2>%b(&desc)</h2>
3225
}
3226
style_submenu_element("Diff", "%R/vdiff?branch=%T", zBrName);
3227
}else
3228
if( zTagName
3229
&& matchStyle==MS_EXACT
3230
&& zBrName==0
3231
&& !PB("nowiki")
3232
&& wiki_render_associated("tag", zTagName, WIKIASSOC_ALL)
3233
){
3234
@ <div class="section">%b(&desc)</div>
3235
} else{
3236
@ <h2>%b(&desc)</h2>
3237
}
3238
blob_reset(&desc);
3239
3240
/* Report any errors. */
3241
if( zError ){
3242
@ <p class="generalError">%h(zError)</p>
3243
}
3244
3245
/* Swap zNewer and zOlder buttons if we display oldestfirst */
3246
if( PB("oldestfirst") ){
3247
char *zSwap = zNewerButton;
3248
char *zSwapLabel = zNewerButtonLabel;
3249
zNewerButton = zOlderButton;
3250
zNewerButtonLabel = zOlderButtonLabel;
3251
zOlderButton = zSwap;
3252
zOlderButtonLabel = zSwapLabel;
3253
}
3254
3255
if( zNewerButton ){
3256
@ %z(chref("button","%s",zNewerButton))%h(zNewerButtonLabel)\
3257
@ &nbsp;&uarr;</a>
3258
}
3259
cgi_check_for_malice();
3260
{
3261
Matcher *pLeftBranch;
3262
const char *zPattern = P("sl");
3263
if( zPattern!=0 ){
3264
MatchStyle ms;
3265
if( zMatchStyle!=0 ){
3266
ms = matchStyle;
3267
}else{
3268
ms = strpbrk(zPattern,"*[?")!=0 ? MS_GLOB : MS_BRLIST;
3269
}
3270
pLeftBranch = match_create(ms,zPattern);
3271
}else{
3272
pLeftBranch = match_create(matchStyle, zBrName?zBrName:zTagName);
3273
}
3274
www_print_timeline(&q, tmFlags, zThisUser, zThisTag, pLeftBranch,
3275
selectedRid, secondaryRid, 0);
3276
match_free(pLeftBranch);
3277
}
3278
db_finalize(&q);
3279
if( zOlderButton ){
3280
@ %z(chref("button","%s",zOlderButton))%h(zOlderButtonLabel)\
3281
@ &nbsp;&darr;</a>
3282
}
3283
document_emit_js(/*handles pikchrs rendered above*/);
3284
blob_reset(&sql);
3285
blob_reset(&desc);
3286
style_finish_page();
3287
}
3288
3289
/*
3290
** Translate a timeline entry into the printable format by
3291
** converting every %-substitutions as follows:
3292
**
3293
** %n newline
3294
** %% a raw %
3295
** %H commit hash
3296
** %h abbreviated commit hash
3297
** %a author name
3298
** %d date
3299
** %c comment (\n, \t replaced by space, \r deleted)
3300
** %b branch
3301
** %t tags
3302
** %p phase (zero or more of: *CURRENT*, *MERGE*, *FORK*,
3303
** *UNPUBLISHED*, *LEAF*, *BRANCH*)
3304
**
3305
** The returned string is obtained from fossil_malloc() and should
3306
** be freed by the caller.
3307
*/
3308
static char *timeline_entry_subst(
3309
const char *zFormat,
3310
int *nLine,
3311
const char *zId,
3312
const char *zDate,
3313
const char *zUser,
3314
const char *zCom,
3315
const char *zBranch,
3316
const char *zTags,
3317
const char *zPhase
3318
){
3319
Blob r, co;
3320
int i, j;
3321
blob_init(&r, 0, 0);
3322
blob_init(&co, 0, 0);
3323
3324
if( 0==zCom ){
3325
zCom = "(NULL)";
3326
}
3327
3328
/* Replace LF and tab with space, delete CR */
3329
while( zCom[0] ){
3330
for(j=0; zCom[j] && zCom[j]!='\r' && zCom[j]!='\n' && zCom[j]!='\t'; j++){}
3331
blob_append(&co, zCom, j);
3332
if( zCom[j]==0 ) break;
3333
if( zCom[j]!='\r')
3334
blob_append(&co, " ", 1);
3335
zCom += j+1;
3336
}
3337
blob_str(&co);
3338
3339
*nLine = 1;
3340
while( zFormat[0] ){
3341
for(i=0; zFormat[i] && zFormat[i]!='%'; i++){}
3342
blob_append(&r, zFormat, i);
3343
if( zFormat[i]==0 ) break;
3344
if( zFormat[i+1]=='%' ){
3345
blob_append(&r, "%", 1);
3346
zFormat += i+2;
3347
}else if( zFormat[i+1]=='n' ){
3348
blob_append(&r, "\n", 1);
3349
*nLine += 1;
3350
zFormat += i+2;
3351
}else if( zFormat[i+1]=='H' ){
3352
blob_append(&r, zId, -1);
3353
zFormat += i+2;
3354
}else if( zFormat[i+1]=='h' ){
3355
char *zFree = 0;
3356
zFree = mprintf("%S", zId);
3357
blob_append(&r, zFree, -1);
3358
fossil_free(zFree);
3359
zFormat += i+2;
3360
}else if( zFormat[i+1]=='d' ){
3361
blob_append(&r, zDate, -1);
3362
zFormat += i+2;
3363
}else if( zFormat[i+1]=='a' ){
3364
blob_append(&r, zUser, -1);
3365
zFormat += i+2;
3366
}else if( zFormat[i+1]=='c' ){
3367
blob_append(&r, co.aData, -1);
3368
zFormat += i+2;
3369
}else if( zFormat[i+1]=='b' ){
3370
if( zBranch ) blob_append(&r, zBranch, -1);
3371
zFormat += i+2;
3372
}else if( zFormat[i+1]=='t' ){
3373
blob_append(&r, zTags, -1);
3374
zFormat += i+2;
3375
}else if( zFormat[i+1]=='p' ){
3376
blob_append(&r, zPhase, -1);
3377
zFormat += i+2;
3378
}else{
3379
blob_append(&r, zFormat+i, 1);
3380
zFormat += i+1;
3381
}
3382
}
3383
fossil_free(co.aData);
3384
blob_str(&r);
3385
return r.aData;
3386
}
3387
3388
/*
3389
** The input query q selects various records. Print a human-readable
3390
** summary of those records.
3391
**
3392
** Limit number of lines or entries printed to nLimit. If nLimit is zero
3393
** there is no limit. If nLimit is greater than zero, limit the number of
3394
** complete entries printed. If nLimit is less than zero, attempt to limit
3395
** the number of lines printed (this is basically the legacy behavior).
3396
** The line limit, if used, is approximate because it is only checked on a
3397
** per-entry basis. If verbose mode, the file name details are considered
3398
** to be part of the entry.
3399
**
3400
** The query should return these columns:
3401
**
3402
** 0. rid
3403
** 1. uuid
3404
** 2. Date/Time
3405
** 3. Comment string, user, and tags
3406
** 4. Number of non-merge children
3407
** 5. Number of parents
3408
** 6. mtime
3409
** 7. branch
3410
** 8. event-type: 'ci', 'w', 't', 'f', and so forth.
3411
** 9. comment
3412
** 10. user
3413
** 11. tags
3414
*/
3415
void print_timeline(Stmt *q, int nLimit, int width, const char *zFormat,
3416
int verboseFlag){
3417
int nAbsLimit = (nLimit >= 0) ? nLimit : -nLimit;
3418
int nLine = 0;
3419
int nEntry = 0;
3420
char zPrevDate[20];
3421
const char *zCurrentUuid = 0;
3422
int fchngQueryInit = 0; /* True if fchngQuery is initialized */
3423
Stmt fchngQuery; /* Query for file changes on check-ins */
3424
int rc;
3425
/* True: separate entries with a newline after file listing */
3426
int bVerboseNL = (zFormat &&
3427
(fossil_strcmp(zFormat, TIMELINE_FMT_ONELINE)!=0));
3428
/* True: separate entries with a newline even with no file listing */
3429
int bNoVerboseNL = (zFormat &&
3430
(fossil_strcmp(zFormat, TIMELINE_FMT_MEDIUM)==0 ||
3431
fossil_strcmp(zFormat, TIMELINE_FMT_FULL)==0));
3432
3433
zPrevDate[0] = 0;
3434
if( g.localOpen ){
3435
int rid = db_lget_int("checkout", 0);
3436
zCurrentUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
3437
}
3438
3439
while( (rc=db_step(q))==SQLITE_ROW ){
3440
int rid = db_column_int(q, 0);
3441
const char *zId = db_column_text(q, 1);
3442
const char *zDate = db_column_text(q, 2);
3443
const char *zCom = db_column_text(q, 3);
3444
int nChild = db_column_int(q, 4);
3445
int nParent = db_column_int(q, 5);
3446
const char *zBranch = db_column_text(q, 7);
3447
const char *zType = db_column_text(q, 8);
3448
const char *zComShort = db_column_text(q, 9);
3449
const char *zUserShort = db_column_text(q, 10);
3450
const char *zTags = db_column_text(q, 11);
3451
char *zFree = 0;
3452
int n = 0;
3453
char zPrefix[80];
3454
3455
if( nAbsLimit!=0 ){
3456
if( nLimit<0 && nLine>=nAbsLimit ){
3457
if( !g.fQuiet ){
3458
fossil_print("--- line limit (%d) reached ---\n", nAbsLimit);
3459
}
3460
break; /* line count limit hit, stop. */
3461
}else if( nEntry>=nAbsLimit ){
3462
if( !g.fQuiet ){
3463
fossil_print("--- entry limit (%d) reached ---\n", nAbsLimit);
3464
}
3465
break; /* entry count limit hit, stop. */
3466
}
3467
}
3468
if( zFormat == 0 && fossil_strnicmp(zDate, zPrevDate, 10) ){
3469
fossil_print("=== %.10s ===\n", zDate);
3470
memcpy(zPrevDate, zDate, 10);
3471
nLine++; /* record another line */
3472
}
3473
if( zCom==0 ) zCom = "";
3474
if( zFormat == 0 )
3475
fossil_print("%.8s ", &zDate[11]);
3476
zPrefix[0] = 0;
3477
if( nParent>1 ){
3478
sqlite3_snprintf(sizeof(zPrefix), zPrefix, "*MERGE* ");
3479
n = strlen(zPrefix);
3480
}
3481
if( nChild>1 ){
3482
const char *zBrType;
3483
if( count_nonbranch_children(rid)>1 ){
3484
zBrType = "*FORK* ";
3485
}else{
3486
zBrType = "*BRANCH* ";
3487
}
3488
sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], zBrType);
3489
n = strlen(zPrefix);
3490
}
3491
if( fossil_strcmp(zCurrentUuid,zId)==0 ){
3492
sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*CURRENT* ");
3493
n += strlen(zPrefix+n);
3494
}
3495
if( content_is_private(rid) ){
3496
sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*UNPUBLISHED* ");
3497
n += strlen(zPrefix+n);
3498
}
3499
if( zType && zType[0]=='w'
3500
&& (zCom[0]=='+' || zCom[0]=='-' || zCom[0]==':')
3501
){
3502
/* Special processing for Wiki comments */
3503
if(!zComShort || !*zComShort){
3504
/* Shouldn't be possible, but just in case... */
3505
zComShort = " ";
3506
}
3507
if( zCom[0]=='+' ){
3508
zFree = mprintf("[%S] Add wiki page \"%s\" (user: %s)",
3509
zId, zComShort+1, zUserShort);
3510
}else if( zCom[0]=='-' ){
3511
zFree = mprintf("[%S] Delete wiki page \"%s\" (user: %s)",
3512
zId, zComShort+1, zUserShort);
3513
}else{
3514
zFree = mprintf("[%S] Edit to wiki page \"%s\" (user: %s)",
3515
zId, zComShort+1, zUserShort);
3516
}
3517
}else{
3518
zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom);
3519
}
3520
3521
if( zFormat ){
3522
char *zEntry;
3523
int nEntryLine = 0;
3524
if( nChild==0 ){
3525
sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*LEAF* ");
3526
}
3527
zEntry = timeline_entry_subst(zFormat, &nEntryLine, zId, zDate,
3528
zUserShort, zComShort, zBranch, zTags,
3529
zPrefix);
3530
nLine += nEntryLine;
3531
fossil_print("%s\n", zEntry);
3532
fossil_free(zEntry);
3533
}
3534
else{
3535
/* record another X lines */
3536
nLine += comment_print(zFree, zCom, 9, width, get_comment_format());
3537
}
3538
fossil_free(zFree);
3539
3540
if(verboseFlag){
3541
if( !fchngQueryInit ){
3542
db_prepare(&fchngQuery,
3543
"SELECT (pid<=0) AS isnew,"
3544
" (fid==0) AS isdel,"
3545
" (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
3546
" (SELECT uuid FROM blob WHERE rid=fid),"
3547
" (SELECT uuid FROM blob WHERE rid=pid)"
3548
" FROM mlink"
3549
" WHERE mid=:mid AND pid!=fid AND NOT mlink.isaux"
3550
" ORDER BY 3 /*sort*/"
3551
);
3552
fchngQueryInit = 1;
3553
}
3554
db_bind_int(&fchngQuery, ":mid", rid);
3555
while( db_step(&fchngQuery)==SQLITE_ROW ){
3556
const char *zFilename = db_column_text(&fchngQuery, 2);
3557
int isNew = db_column_int(&fchngQuery, 0);
3558
int isDel = db_column_int(&fchngQuery, 1);
3559
if( isNew ){
3560
fossil_print(" ADDED %s\n",zFilename);
3561
}else if( isDel ){
3562
fossil_print(" DELETED %s\n",zFilename);
3563
}else{
3564
fossil_print(" EDITED %s\n", zFilename);
3565
}
3566
nLine++; /* record another line */
3567
}
3568
db_reset(&fchngQuery);
3569
if( bVerboseNL ) fossil_print("\n");
3570
}else{
3571
if( bNoVerboseNL ) fossil_print("\n");
3572
}
3573
3574
nEntry++; /* record another complete entry */
3575
}
3576
if( rc==SQLITE_DONE ){
3577
/* Did the underlying query actually have all entries? */
3578
if( nAbsLimit==0 ){
3579
if( !g.fQuiet ){
3580
fossil_print("+++ end of timeline (%d) +++\n", nEntry);
3581
}
3582
}else{
3583
if( !g.fQuiet ){
3584
fossil_print("+++ no more data (%d) +++\n", nEntry);
3585
}
3586
}
3587
}
3588
if( fchngQueryInit ) db_finalize(&fchngQuery);
3589
}
3590
3591
/*
3592
** wiki_to_text(TEXT)
3593
**
3594
** Return a text rendering of Fossil-Wiki TEXT, intended for display
3595
** on a timeline. The timeline-plaintext and timeline-hard-newlines
3596
** settings are considered when doing this rendering.
3597
*/
3598
static void wiki_to_text_sqlfunc(
3599
sqlite3_context *context,
3600
int argc,
3601
sqlite3_value **argv
3602
){
3603
const char *zIn, *zOut;
3604
int nIn, nOut;
3605
Blob in, html, txt;
3606
zIn = (const char*)sqlite3_value_text(argv[0]);
3607
if( zIn==0 ) return;
3608
nIn = sqlite3_value_bytes(argv[0]);
3609
blob_init(&in, zIn, nIn);
3610
blob_init(&html, 0, 0);
3611
wiki_convert(&in, &html, wiki_convert_flags(0));
3612
blob_reset(&in);
3613
blob_init(&txt, 0, 0);
3614
html_to_plaintext(blob_str(&html), &txt, 0);
3615
blob_reset(&html);
3616
nOut = blob_size(&txt);
3617
zOut = blob_str(&txt);
3618
while( fossil_isspace(zOut[0]) ){ zOut++; nOut--; }
3619
while( nOut>0 && fossil_isspace(zOut[nOut-1]) ){ nOut--; }
3620
sqlite3_result_text(context, zOut, nOut, SQLITE_TRANSIENT);
3621
blob_reset(&txt);
3622
}
3623
3624
/*
3625
** Return a pointer to a static string that forms the basis for
3626
** a timeline query for display on a TTY.
3627
*/
3628
const char *timeline_query_for_tty(void){
3629
static int once = 0;
3630
static const char zBaseSql[] =
3631
@ SELECT
3632
@ blob.rid AS rid,
3633
@ uuid,
3634
@ datetime(event.mtime,toLocal()) AS mDateTime,
3635
@ wiki_to_text(coalesce(ecomment,comment))
3636
@ || ' (user: ' || coalesce(euser,user,'?')
3637
@ || (SELECT case when length(x)>0 then ' tags: ' || x else '' end
3638
@ FROM (SELECT group_concat(substr(tagname,5), ', ') AS x
3639
@ FROM tag, tagxref
3640
@ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
3641
@ AND tagxref.rid=blob.rid AND tagxref.tagtype>0))
3642
@ || ')' as comment,
3643
@ (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim)
3644
@ AS primPlinkCount,
3645
@ (SELECT count(*) FROM plink WHERE cid=blob.rid) AS plinkCount,
3646
@ event.mtime AS mtime,
3647
@ tagxref.value AS branch,
3648
@ event.type
3649
@ , coalesce(ecomment,comment) AS comment0
3650
@ , coalesce(euser,user,'?') AS user0
3651
@ , (SELECT case when length(x)>0 then x else '' end
3652
@ FROM (SELECT group_concat(substr(tagname,5), ', ') AS x
3653
@ FROM tag, tagxref
3654
@ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
3655
@ AND tagxref.rid=blob.rid AND tagxref.tagtype>0)) AS tags
3656
@ FROM tag CROSS JOIN event CROSS JOIN blob
3657
@ LEFT JOIN tagxref ON tagxref.tagid=tag.tagid
3658
@ AND tagxref.tagtype>0
3659
@ AND tagxref.rid=blob.rid
3660
@ WHERE blob.rid=event.objid
3661
@ AND tag.tagname='branch'
3662
;
3663
if( !once && g.db ){
3664
once = 1;
3665
sqlite3_create_function(g.db, "wiki_to_text", 1, SQLITE_UTF8, 0,
3666
wiki_to_text_sqlfunc, 0, 0);
3667
}
3668
return zBaseSql;
3669
}
3670
3671
/*
3672
** Return true if the input string is a date in the ISO 8601 format:
3673
** YYYY-MM-DD.
3674
*/
3675
static int isIsoDate(const char *z){
3676
return strlen(z)==10
3677
&& z[4]=='-'
3678
&& z[7]=='-'
3679
&& fossil_isdigit(z[0])
3680
&& fossil_isdigit(z[5]);
3681
}
3682
3683
/*
3684
** Return true if the input string can be converted to a Julian day.
3685
*/
3686
static int fossil_is_julianday(const char *zDate){
3687
return db_int(0, "SELECT EXISTS (SELECT julianday(%Q) AS jd"
3688
" WHERE jd IS NOT NULL)", zDate);
3689
}
3690
3691
3692
/*
3693
** COMMAND: timeline
3694
**
3695
** Usage: %fossil timeline ?WHEN? ?CHECKIN|DATETIME? ?OPTIONS?
3696
**
3697
** Print a summary of activity going backwards in date and time
3698
** specified or from the current date and time if no arguments
3699
** are given. The WHEN argument can be any unique abbreviation
3700
** of one of these keywords:
3701
**
3702
** before
3703
** after
3704
** descendants | children
3705
** ancestors | parents
3706
**
3707
** The CHECKIN can be any unique prefix of 4 characters or more. You
3708
** can also say "current" for the current version.
3709
**
3710
** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
3711
** year-month-day form, it may be truncated, the "T" may be replaced by
3712
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
3713
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
3714
** means UTC.
3715
**
3716
**
3717
** Options:
3718
** -b|--branch BRANCH Show only items on the branch named BRANCH
3719
** -c|--current-branch Show only items on the current branch
3720
** -F|--format Entry format. Values "oneline", "medium", and "full"
3721
** get mapped to the full options below. Otherwise a
3722
** string which can contain these placeholders:
3723
** %n newline
3724
** %% a raw %
3725
** %H commit hash
3726
** %h abbreviated commit hash
3727
** %a author name
3728
** %d date
3729
** %c comment (NL, TAB replaced by space, LF erased)
3730
** %b branch
3731
** %t tags
3732
** %p phase: zero or more of *CURRENT*, *MERGE*,
3733
** *FORK*, *UNPUBLISHED*, *LEAF*, *BRANCH*
3734
** --oneline Show only short hash and comment for each entry
3735
** --medium Medium-verbose entry formatting
3736
** --full Extra verbose entry formatting
3737
** -n|--limit N If N is positive, output the first N entries. If
3738
** N is negative, output the first -N lines. If N is
3739
** zero, no limit. Default is -20 meaning 20 lines.
3740
** --offset P Skip P changes
3741
** -p|--path PATH Output items affecting PATH only.
3742
** PATH can be a file or a subdirectory.
3743
** -q|--quiet Do not print notifications at the end of the timeline.
3744
** -r|--reverse Show items in chronological order.
3745
** -R REPO_FILE Specifies the repository db to use. Default is
3746
** the current check-out's repository.
3747
** --sql Show the SQL used to generate the timeline
3748
** -t|--type TYPE Output items from the given types only, such as:
3749
** ci = file commits only
3750
** e = technical notes only
3751
** f = forum posts only
3752
** t = tickets only
3753
** w = wiki commits only
3754
** -u|--for-user USER Only show items associated with USER
3755
** -v|--verbose Output the list of files changed by each commit
3756
** and the type of each change (edited, deleted,
3757
** etc.) after the check-in comment.
3758
** -W|--width N Width of lines (default is to auto-detect). N must be
3759
** either greater than 20 or it must be zero 0 to
3760
** indicate no limit, resulting in a single line per
3761
** entry.
3762
*/
3763
void timeline_cmd(void){
3764
Stmt q;
3765
int n, k, width;
3766
const char *zLimit;
3767
const char *zWidth;
3768
const char *zOffset;
3769
const char *zType;
3770
const char *zUser;
3771
char *zOrigin;
3772
char *zDate;
3773
Blob sql;
3774
int objid = 0;
3775
Blob uuid;
3776
int mode = TIMELINE_MODE_NONE;
3777
int verboseFlag = 0;
3778
int reverseFlag = 0;
3779
int iOffset;
3780
const char *zFilePattern = 0;
3781
const char *zFormat = 0;
3782
const char *zBr = 0;
3783
Blob treeName;
3784
int showSql = 0;
3785
3786
verboseFlag = find_option("verbose","v", 0)!=0;
3787
if( !verboseFlag){
3788
verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */
3789
}
3790
db_find_and_open_repository(0, 0);
3791
zLimit = find_option("limit","n",1);
3792
zWidth = find_option("width","W",1);
3793
zType = find_option("type","t",1);
3794
zUser = find_option("for-user","u",1);
3795
zFilePattern = find_option("path","p",1);
3796
zFormat = find_option("format","F",1);
3797
zBr = find_option("branch","b",1);
3798
if( find_option("current-branch","c",0)!=0 ){
3799
if( !g.localOpen ){
3800
fossil_fatal("not within an open check-out");
3801
}else{
3802
int vid = db_lget_int("checkout", 0);
3803
zBr = db_text(0, "SELECT value FROM tagxref WHERE rid=%d AND tagid=%d",
3804
vid, TAG_BRANCH);
3805
}
3806
}
3807
if( find_option("oneline",0,0)!= 0 || fossil_strcmp(zFormat,"oneline")==0 ){
3808
zFormat = TIMELINE_FMT_ONELINE;
3809
}
3810
if( find_option("medium",0,0)!= 0 || fossil_strcmp(zFormat,"medium")==0 ){
3811
zFormat = TIMELINE_FMT_MEDIUM;
3812
}
3813
if( find_option("full",0,0)!= 0 || fossil_strcmp(zFormat,"full")==0 ){
3814
zFormat = TIMELINE_FMT_FULL;
3815
}
3816
showSql = find_option("sql",0,0)!=0;
3817
3818
if( !zLimit ){
3819
zLimit = find_option("count",0,1);
3820
}
3821
if( zLimit ){
3822
n = atoi(zLimit);
3823
}else{
3824
n = -20;
3825
}
3826
if( zWidth ){
3827
width = atoi(zWidth);
3828
if( (width!=0) && (width<=20) ){
3829
fossil_fatal("-W|--width value must be >20 or 0");
3830
}
3831
}else{
3832
width = -1;
3833
}
3834
zOffset = find_option("offset",0,1);
3835
iOffset = zOffset ? atoi(zOffset) : 0;
3836
reverseFlag = find_option("reverse","r",0)!=0;
3837
3838
/* We should be done with options.. */
3839
verify_all_options();
3840
3841
if( g.argc>=4 ){
3842
k = strlen(g.argv[2]);
3843
if( strncmp(g.argv[2],"before",k)==0 ){
3844
mode = TIMELINE_MODE_BEFORE;
3845
}else if( strncmp(g.argv[2],"after",k)==0 && k>1 ){
3846
mode = TIMELINE_MODE_AFTER;
3847
}else if( strncmp(g.argv[2],"descendants",k)==0 ){
3848
mode = TIMELINE_MODE_CHILDREN;
3849
}else if( strncmp(g.argv[2],"children",k)==0 ){
3850
mode = TIMELINE_MODE_CHILDREN;
3851
}else if( strncmp(g.argv[2],"ancestors",k)==0 && k>1 ){
3852
mode = TIMELINE_MODE_PARENTS;
3853
}else if( strncmp(g.argv[2],"parents",k)==0 ){
3854
mode = TIMELINE_MODE_PARENTS;
3855
}else if(!zType && !zLimit){
3856
usage("?WHEN? ?CHECKIN|DATETIME? ?-n|--limit #? ?-t|--type TYPE? "
3857
"?-W|--width WIDTH? ?-p|--path PATH? ?-r|--reverse?");
3858
}
3859
if( '-' != *g.argv[3] ){
3860
zOrigin = g.argv[3];
3861
}else{
3862
zOrigin = "now";
3863
}
3864
}else if( g.argc==3 ){
3865
zOrigin = g.argv[2];
3866
}else{
3867
zOrigin = "now";
3868
}
3869
k = strlen(zOrigin);
3870
blob_zero(&uuid);
3871
blob_append(&uuid, zOrigin, -1);
3872
if( fossil_strcmp(zOrigin, "now")==0 ){
3873
if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
3874
fossil_fatal("cannot compute descendants or ancestors of a date");
3875
}
3876
zDate = mprintf("(SELECT datetime('now'))");
3877
}else if( strncmp(zOrigin, "current", k)==0 ){
3878
if( !g.localOpen ){
3879
fossil_fatal("must be within a local check-out to use 'current'");
3880
}
3881
objid = db_lget_int("checkout",0);
3882
zDate = mprintf("(SELECT mtime FROM plink WHERE cid=%d)", objid);
3883
}else if( fossil_is_julianday(zOrigin) ){
3884
const char *zShift = "";
3885
if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
3886
fossil_fatal("cannot compute descendants or ancestors of a date");
3887
}
3888
if( mode==TIMELINE_MODE_NONE ){
3889
if( isIsoDate(zOrigin) ) zShift = ",'+1 day'";
3890
}
3891
zDate = mprintf("(SELECT julianday(%Q%s, fromLocal()))", zOrigin, zShift);
3892
}else if( name_to_uuid(&uuid, 0, "*")==0 ){
3893
objid = db_int(0, "SELECT rid FROM blob WHERE uuid=%B", &uuid);
3894
zDate = mprintf("(SELECT mtime FROM event WHERE objid=%d)", objid);
3895
}else{
3896
fossil_fatal("unknown check-in or invalid date: %s", zOrigin);
3897
}
3898
3899
if( zFilePattern ){
3900
if( zType==0 ){
3901
/* When zFilePattern is specified and type is not specified, only show
3902
* file check-ins */
3903
zType="ci";
3904
}
3905
file_tree_name(zFilePattern, &treeName, 0, 1);
3906
if( fossil_strcmp(blob_str(&treeName), ".")==0 ){
3907
/* When zTreeName refers to g.zLocalRoot, it's like not specifying
3908
* zFilePattern. */
3909
zFilePattern = 0;
3910
}
3911
}
3912
3913
if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
3914
blob_zero(&sql);
3915
if( mode==TIMELINE_MODE_AFTER ){
3916
/* Extra outer select to get older rows in reverse order */
3917
blob_append(&sql, "SELECT *\nFROM (", -1);
3918
}
3919
blob_append(&sql, timeline_query_for_tty(), -1);
3920
blob_append_sql(&sql, "\n AND event.mtime %s %s",
3921
( mode==TIMELINE_MODE_BEFORE ||
3922
mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
3923
);
3924
if( zType && (zType[0]!='a') ){
3925
blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3926
}
3927
if( zUser && (zUser[0]!='\0') ){
3928
blob_append_sql(&sql, "\n AND user0=%Q ", zUser);
3929
}
3930
3931
/* When zFilePattern is specified, compute complete ancestry;
3932
* limit later at print_timeline() */
3933
if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
3934
db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
3935
if( mode==TIMELINE_MODE_CHILDREN ){
3936
compute_descendants(objid, (zFilePattern ? 0 : n));
3937
}else{
3938
compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
3939
}
3940
blob_append_sql(&sql, "\n AND blob.rid IN ok");
3941
}
3942
if( zFilePattern ){
3943
blob_append(&sql,
3944
"\n AND EXISTS(SELECT 1 FROM mlink\n"
3945
" WHERE mlink.mid=event.objid\n"
3946
" AND mlink.fnid IN ", -1);
3947
if( filenames_are_case_sensitive() ){
3948
blob_append_sql(&sql,
3949
"(SELECT fnid FROM filename"
3950
" WHERE name=%Q"
3951
" OR name GLOB '%q/*')",
3952
blob_str(&treeName), blob_str(&treeName));
3953
}else{
3954
blob_append_sql(&sql,
3955
"(SELECT fnid FROM filename"
3956
" WHERE name=%Q COLLATE nocase"
3957
" OR lower(name) GLOB lower('%q/*'))",
3958
blob_str(&treeName), blob_str(&treeName));
3959
}
3960
blob_append(&sql, ")", -1);
3961
}
3962
if( zBr ){
3963
blob_append_sql(&sql,
3964
"\n AND blob.rid IN (\n" /* Commits */
3965
" SELECT rid FROM tagxref NATURAL JOIN tag\n"
3966
" WHERE tagtype>0 AND tagname='sym-%q'\n"
3967
" UNION\n" /* Tags */
3968
" SELECT srcid FROM tagxref WHERE origid IN (\n"
3969
" SELECT rid FROM tagxref NATURAL JOIN tag\n"
3970
" WHERE tagname='sym-%q')\n"
3971
" UNION\n" /* Branch wikis */
3972
" SELECT objid FROM event WHERE comment LIKE '_branch/%q'\n"
3973
" UNION\n" /* Check-in wikis */
3974
" SELECT e.objid FROM event e\n"
3975
" INNER JOIN blob b ON b.uuid=substr(e.comment, 10)\n"
3976
" AND e.comment LIKE '_checkin/%%'\n"
3977
" LEFT JOIN tagxref tx ON tx.rid=b.rid AND tx.tagid=%d\n"
3978
" WHERE tx.value='%q'\n"
3979
")\n" /* No merge closures */
3980
" AND (tagxref.value IS NULL OR tagxref.value='%q')",
3981
zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
3982
}
3983
3984
if( mode==TIMELINE_MODE_AFTER ){
3985
int lim = n;
3986
if( n == 0 ){
3987
lim = -1; /* 0 means no limit */
3988
}else if( n < 0 ){
3989
lim = -n;
3990
}
3991
/* Complete the above outer select. */
3992
blob_append_sql(&sql,
3993
"\nORDER BY event.mtime LIMIT %d) t ORDER BY t.mDateTime %s",
3994
lim, reverseFlag ? "" : "DESC");
3995
}else{
3996
blob_append_sql(&sql,
3997
"\nORDER BY event.mtime %s", reverseFlag ? "" : "DESC");
3998
}
3999
if( iOffset>0 ){
4000
/* Don't handle LIMIT here, otherwise print_timeline()
4001
* will not determine the end-marker correctly! */
4002
blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
4003
}
4004
if( showSql ){
4005
fossil_print("%s\n", blob_str(&sql));
4006
}
4007
db_prepare_blob(&q, &sql);
4008
blob_reset(&sql);
4009
print_timeline(&q, n, width, zFormat, verboseFlag);
4010
db_finalize(&q);
4011
}
4012
4013
/*
4014
** WEBPAGE: thisdayinhistory
4015
**
4016
** Generate a vanity page that shows project activity for the current
4017
** day of the year for various years in the history of the project.
4018
**
4019
** Query parameters:
4020
**
4021
** today=DATE Use DATE as today's date
4022
*/
4023
void thisdayinhistory_page(void){
4024
static int aYearsAgo[] = { 1,2,3,4,5,10,15,20,25,30,40,50,75,100 };
4025
const char *zToday;
4026
char *zStartOfProject;
4027
int i;
4028
Stmt q;
4029
char *z;
4030
int bZulu = 0;
4031
4032
login_check_credentials();
4033
if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum) ){
4034
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
4035
return;
4036
}
4037
style_set_current_feature("timeline");
4038
style_header("Today In History");
4039
zToday = (char*)P("today");
4040
if( zToday ){
4041
zToday = timeline_expand_datetime(zToday, &bZulu);
4042
if( !fossil_isdate(zToday) ) zToday = 0;
4043
}
4044
if( zToday==0 ){
4045
zToday = db_text(0, "SELECT date('now',toLocal())");
4046
}
4047
@ <h1>This Day In History For %h(zToday)</h1>
4048
z = db_text(0, "SELECT date(%Q,'-1 day')", zToday);
4049
style_submenu_element("Yesterday", "%R/thisdayinhistory?today=%t", z);
4050
z = db_text(0, "SELECT date(%Q,'+1 day')", zToday);
4051
style_submenu_element("Tomorrow", "%R/thisdayinhistory?today=%t", z);
4052
zStartOfProject = db_text(0,
4053
"SELECT datetime(min(mtime),toLocal(),'startofday') FROM event;"
4054
);
4055
timeline_temp_table();
4056
db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
4057
for(i=0; i<(int)(sizeof(aYearsAgo)/sizeof(aYearsAgo[0])); i++){
4058
int iAgo = aYearsAgo[i];
4059
char *zThis = db_text(0, "SELECT date(%Q,'-%d years')", zToday, iAgo);
4060
Blob sql;
4061
char *zId;
4062
if( strcmp(zThis, zStartOfProject)<0 ) break;
4063
blob_init(&sql, 0, 0);
4064
blob_append(&sql, "INSERT OR IGNORE INTO timeline ", -1);
4065
blob_append(&sql, timeline_query_for_www(), -1);
4066
blob_append_sql(&sql,
4067
" AND %Q=date(event.mtime,toLocal()) "
4068
" AND event.mtime BETWEEN julianday(%Q,'-1 day')"
4069
" AND julianday(%Q,'+2 days')",
4070
zThis, zThis, zThis
4071
);
4072
db_multi_exec("DELETE FROM timeline; %s;", blob_sql_text(&sql));
4073
blob_reset(&sql);
4074
if( db_int(0, "SELECT count(*) FROM timeline")==0 ){
4075
continue;
4076
}
4077
zId = db_text(0, "SELECT timestamp FROM timeline"
4078
" ORDER BY sortby DESC LIMIT 1");
4079
@ <h2>%d(iAgo) Year%s(iAgo>1?"s":"") Ago
4080
@ <small>%z(href("%R/timeline?c=%t",zId))(more context)</a>\
4081
@ </small></h2>
4082
www_print_timeline(&q, TIMELINE_GRAPH, 0, 0, 0, 0, 0, 0);
4083
}
4084
db_finalize(&q);
4085
style_finish_page();
4086
}
4087
4088
4089
/*
4090
** COMMAND: test-timewarp-list
4091
**
4092
** Usage: %fossil test-timewarp-list ?-v|---verbose?
4093
**
4094
** Display all instances of child check-ins that appear earlier in time
4095
** than their parent. If the -v|--verbose option is provided, both the
4096
** parent and child check-ins and their times are shown.
4097
*/
4098
void test_timewarp_cmd(void){
4099
Stmt q;
4100
int verboseFlag;
4101
4102
db_find_and_open_repository(0, 0);
4103
verboseFlag = find_option("verbose", "v", 0)!=0;
4104
if( !verboseFlag ){
4105
verboseFlag = find_option("detail", 0, 0)!=0; /* deprecated */
4106
}
4107
db_prepare(&q,
4108
"SELECT (SELECT uuid FROM blob WHERE rid=p.cid),"
4109
" (SELECT uuid FROM blob WHERE rid=c.cid),"
4110
" datetime(p.mtime), datetime(c.mtime)"
4111
" FROM plink p, plink c"
4112
" WHERE p.cid=c.pid AND p.mtime>c.mtime"
4113
);
4114
while( db_step(&q)==SQLITE_ROW ){
4115
if( !verboseFlag ){
4116
fossil_print("%s\n", db_column_text(&q, 1));
4117
}else{
4118
fossil_print("%.14s -> %.14s %s -> %s\n",
4119
db_column_text(&q, 0),
4120
db_column_text(&q, 1),
4121
db_column_text(&q, 2),
4122
db_column_text(&q, 3));
4123
}
4124
}
4125
db_finalize(&q);
4126
}
4127
4128
/*
4129
** WEBPAGE: timewarps
4130
**
4131
** Show all check-ins that are "timewarps". A timewarp is a
4132
** check-in that occurs before its parent, according to the
4133
** timestamp information on the check-in. This can only actually
4134
** happen, of course, if a users system clock is set incorrectly.
4135
*/
4136
void test_timewarp_page(void){
4137
Stmt q;
4138
int cnt = 0;
4139
4140
login_check_credentials();
4141
if( !g.perm.Read || !g.perm.Hyperlink ){
4142
login_needed(g.anon.Read && g.anon.Hyperlink);
4143
return;
4144
}
4145
style_header("Instances of timewarp");
4146
db_prepare(&q,
4147
"SELECT blob.uuid, "
4148
" date(ce.mtime),"
4149
" pe.mtime>ce.mtime,"
4150
" coalesce(ce.euser,ce.user)"
4151
" FROM plink p, plink c, blob, event pe, event ce"
4152
" WHERE p.cid=c.pid AND p.mtime>c.mtime"
4153
" AND blob.rid=c.cid"
4154
" AND pe.objid=p.cid"
4155
" AND ce.objid=c.cid"
4156
" ORDER BY 2 DESC"
4157
);
4158
while( db_step(&q)==SQLITE_ROW ){
4159
const char *zCkin = db_column_text(&q, 0);
4160
const char *zDate = db_column_text(&q, 1);
4161
const char *zStatus = db_column_int(&q,2) ? "Open"
4162
: "Resolved by editing date";
4163
const char *zUser = db_column_text(&q, 3);
4164
char *zHref = href("%R/timeline?c=%S", zCkin);
4165
if( cnt==0 ){
4166
style_table_sorter();
4167
@ <div class="brlist">
4168
@ <table class='sortable' data-column-types='tttt' data-init-sort='2'>
4169
@ <thead><tr>
4170
@ <th>Check-in</th>
4171
@ <th>Date</th>
4172
@ <th>User</th>
4173
@ <th>Status</th>
4174
@ </tr></thead><tbody>
4175
}
4176
@ <tr>
4177
@ <td>%s(zHref)%S(zCkin)</a></td>
4178
@ <td>%s(zHref)%s(zDate)</a></td>
4179
@ <td>%h(zUser)</td>
4180
@ <td>%s(zStatus)</td>
4181
@ </tr>
4182
fossil_free(zHref);
4183
cnt++;
4184
}
4185
db_finalize(&q);
4186
if( cnt==0 ){
4187
@ <p>No timewarps in this repository</p>
4188
}else{
4189
@ </tbody></table></div>
4190
}
4191
style_finish_page();
4192
}
4193

Keyboard Shortcuts

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