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