Fossil SCM

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

Keyboard Shortcuts

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