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         @ &bull;
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)&larr;%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 &rarr; 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:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
598       }else if( zType[0]=='e' && tagid ){
599         cgi_printf("technote:&nbsp;");
600         hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
601       }else{
602         cgi_printf("artifact:&nbsp;%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:&nbsp;%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:&nbsp;%z%h</a>", href("%z",zLink), zDispUser);
617     }else{
618       cgi_printf("user:&nbsp;%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:&nbsp;%s", blob_str(&links));
645         blob_reset(&links);
646       }else{
647         cgi_printf(" tags:&nbsp;%h", zTagList);
648       }
649     }
650 
651     if( tmFlags & TIMELINE_SHOWRID ){
652       int srcId = delta_source_rid(rid);
653       if( srcId ){
654         cgi_printf(" id:&nbsp;%d&larr;%d", rid, srcId);
655       }else{
656         cgi_printf(" id:&nbsp;%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&larr;%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) &rarr; %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           @ &nbsp; %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) &rarr; %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) &rarr; %s(zA)%h(zFilename)%s(zId)</a> %s(zUnpub)
748           }else{
749             @ <li>%s(zA)%h(zFilename)</a>%s(zId) &nbsp; %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(&regexp, 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     @ &nbsp;&uarr;</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     @ &nbsp;&darr;</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