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