From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/timeline.c | 4261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4261 insertions(+) create mode 100644 src/timeline.c (limited to 'src/timeline.c') diff --git a/src/timeline.c b/src/timeline.c new file mode 100644 index 0000000..2942e40 --- /dev/null +++ b/src/timeline.c @@ -0,0 +1,4261 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code to implement the timeline web page +** +*/ +#include "config.h" +#include +#include +#include "timeline.h" + +/* +** The value of one second in Julian day notation +*/ +#define ONE_SECOND (1.0/86400.0) + +/* +** timeline mode options +*/ +#define TIMELINE_MODE_NONE 0 +#define TIMELINE_MODE_BEFORE 1 +#define TIMELINE_MODE_AFTER 2 +#define TIMELINE_MODE_CHILDREN 3 +#define TIMELINE_MODE_PARENTS 4 + +#define TIMELINE_FMT_ONELINE \ + "%h %c" +#define TIMELINE_FMT_MEDIUM \ + "Commit: %h%nDate: %d%nAuthor: %a%nComment: %c" +#define TIMELINE_FMT_FULL \ + "Commit: %H%nDate: %d%nAuthor: %a%nComment: %c%n"\ + "Branch: %b%nTags: %t%nPhase: %p" +/* +** Add an appropriate tag to the output if "rid" is unpublished (private) +*/ +#define UNPUB_TAG "(unpublished)" +void tag_private_status(int rid){ + if( content_is_private(rid) ){ + cgi_printf(" %s", UNPUB_TAG); + } +} + +/* +** Generate a hyperlink to a version. +*/ +void hyperlink_to_version(const char *zVerHash){ + if( g.perm.Hyperlink ){ + @ %z(chref("timelineHistLink","%R/info/%!S",zVerHash))[%S(zVerHash)] + }else{ + @ [%S(zVerHash)] + } +} + +/* +** Generate a hyperlink to a date & time. +*/ +void hyperlink_to_date(const char *zDate, const char *zSuffix){ + if( zSuffix==0 ) zSuffix = ""; + if( g.perm.Hyperlink ){ + @ %z(href("%R/timeline?c=%T",zDate))%s(zDate)%s(zSuffix) + }else{ + @ %s(zDate)%s(zSuffix) + } +} + +/* +** Generate a hyperlink to a user. This will link to a timeline showing +** events by that user. If the date+time is specified, then the timeline +** is centered on that date+time. +*/ +void hyperlink_to_user(const char *zU, const char *zD, const char *zSuf){ + if( zU==0 || zU[0]==0 ) zU = "anonymous"; + if( zSuf==0 ) zSuf = ""; + if( g.perm.Hyperlink ){ + if( zD && zD[0] ){ + @ %z(href("%R/timeline?c=%T&u=%T&y=a",zD,zU))%h(zU)%s(zSuf) + }else{ + @ %z(href("%R/timeline?u=%T&y=a",zU))%h(zU)%s(zSuf) + } + }else{ + @ %s(zU) + } +} + +/* +** Allowed flags for the tmFlags argument to www_print_timeline +*/ +#if INTERFACE +#define TIMELINE_ARTID 0x0000001 /* Show artifact IDs on non-check-in lines*/ +#define TIMELINE_LEAFONLY 0x0000002 /* Show "Leaf" but not "Merge", "Fork" etc*/ +#define TIMELINE_BRIEF 0x0000004 /* Combine adjacent elements of same obj */ +#define TIMELINE_GRAPH 0x0000008 /* Compute a graph */ +#define TIMELINE_DISJOINT 0x0000010 /* Elements are not contiguous */ +#define TIMELINE_FCHANGES 0x0000020 /* Detail file changes */ +#define TIMELINE_BRCOLOR 0x0000040 /* Background color by branch name */ +#define TIMELINE_UCOLOR 0x0000080 /* Background color by user */ +#define TIMELINE_FRENAMES 0x0000100 /* Detail only file name changes */ +#define TIMELINE_UNHIDE 0x0000200 /* Unhide check-ins with "hidden" tag */ +#define TIMELINE_SHOWRID 0x0000400 /* Show RID values in addition to hashes */ +#define TIMELINE_BISECT 0x0000800 /* Show supplemental bisect information */ +#define TIMELINE_COMPACT 0x0001000 /* Use the "compact" view style */ +#define TIMELINE_VERBOSE 0x0002000 /* Use the "detailed" view style */ +#define TIMELINE_MODERN 0x0004000 /* Use the "modern" view style */ +#define TIMELINE_COLUMNAR 0x0008000 /* Use the "columns" view style */ +#define TIMELINE_CLASSIC 0x0010000 /* Use the "classic" view style */ +#define TIMELINE_SIMPLE 0x0020000 /* Use the "simple" view style */ +#define TIMELINE_INLINE 0x0033000 /* Mask for views with in-line display */ +#define TIMELINE_VIEWS 0x003f000 /* Mask for all of the view styles */ +#define TIMELINE_NOSCROLL 0x0100000 /* Don't scroll to the selection */ +#define TIMELINE_FILEDIFF 0x0200000 /* Show File differences, not ckin diffs */ +#define TIMELINE_CHPICK 0x0400000 /* Show cherrypick merges */ +#define TIMELINE_FILLGAPS 0x0800000 /* Dotted lines for missing nodes */ +#define TIMELINE_XMERGE 0x1000000 /* Omit merges from off-graph nodes */ +#define TIMELINE_NOTKT 0x2000000 /* Omit extra ticket classes */ +#define TIMELINE_FORUMTXT 0x4000000 /* Render all forum messages */ +#define TIMELINE_REFS 0x8000000 /* Output intended for References tab */ +#define TIMELINE_DELTA 0x10000000 /* Background color shows delta manifests */ +#define TIMELINE_NOCOLOR 0x20000000 /* No colors except for highlights */ +#endif + +/* +** Return a new timelineTable id. +*/ +int timeline_tableid(void){ + static int id = 0; + return id++; +} + +/* +** Return true if the checking identified by "rid" has a valid "closed" +** tag. +*/ +static int has_closed_tag(int rid){ + static Stmt q; + int res = 0; + db_static_prepare(&q, + "SELECT 1 FROM tagxref WHERE rid=$rid AND tagid=%d AND tagtype>0", + TAG_CLOSED); + db_bind_int(&q, "$rid", rid); + res = db_step(&q)==SQLITE_ROW; + db_reset(&q); + return res; +} + +/* +** Return the text of the unformatted +** forum post given by the RID in the argument. +*/ +static void forum_post_content_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int rid = sqlite3_value_int(argv[0]); + Manifest *pPost = manifest_get(rid, CFTYPE_FORUM, 0); + if( pPost ){ + sqlite3_result_text(context, pPost->zWiki, -1, SQLITE_TRANSIENT); + manifest_destroy(pPost); + } +} + + +/* +** This routine generates the default "extra" text after the description +** in a timeline. +** +** Example: "(check-in: [abcdefg], user: drh, tags: trunk)" +** +** This routine is used if no xExtra argument is supplied to +** www_print_timeline(). +*/ +void timeline_extra( + Stmt *pQuery, /* Current row of the timeline query */ + int tmFlags, /* Flags to www_print_timeline() */ + const char *zThisUser, /* Suppress links to this user */ + const char *zThisTag /* Suppress links to this tag */ +){ + int rid = db_column_int(pQuery, 0); + const char *zUuid = db_column_text(pQuery, 1); + const char *zDate = db_column_text(pQuery, 2); + const char *zType = db_column_text(pQuery, 7); + const char *zUser = db_column_text(pQuery, 4); + const char *zTagList = db_column_text(pQuery, 8); + int tagid = db_column_int(pQuery, 9); + const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous"; + + if( (tmFlags & TIMELINE_INLINE)!=0 ){ + cgi_printf("("); + } + + if( (tmFlags & TIMELINE_CLASSIC)==0 ){ + if( zType[0]=='c' ){ + const char *zPrefix = 0; + static int markLeaves = -1; + if( markLeaves<0 ){ + markLeaves = db_get_int("timeline-mark-leaves",1); + if( markLeaves<0 ) markLeaves = 1; + } + if( strcmp(zUuid, MANIFEST_UUID)==0 ){ + /* This will only ever happen when Fossil is drawing a timeline for + ** its own self-host repository. If the timeline shows the specific + ** check-in corresponding to the current executable, then tag that + ** check-in with "self" */ + zPrefix = "self "; + }else if( markLeaves && db_column_int(pQuery,5) ){ + if( markLeaves==1 ){ + zPrefix = has_closed_tag(rid) ? "closed " : "leaf "; + }else{ + zPrefix = has_closed_tag(rid) ? + "Closed-Leaf\n" : + "Leaf\n"; + } + } + cgi_printf("%scheck-in: %z" + "%S ", + zPrefix, href("%R/info/%!S",zUuid),zUuid); + }else if( zType[0]=='e' && tagid ){ + cgi_printf("technote: "); + hyperlink_to_event_tagid(tagid<0?-tagid:tagid); + }else{ + cgi_printf("artifact: %z%S ", + href("%R/info/%!S",zUuid),zUuid); + } + }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' + || zType[0]=='n' || zType[0]=='f'){ + cgi_printf("artifact: %z%S ",href("%R/info/%!S",zUuid),zUuid); + } + + if( (tmFlags & TIMELINE_SIMPLE)!=0 ){ + @ ... + @ + } + + if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){ + char *zLink; + if( zType[0]!='f' || (tmFlags & TIMELINE_FORUMTXT)==0 ){ + zLink = mprintf("%R/timeline?u=%h&c=%t&y=a", zDispUser, zDate); + }else{ + zLink = mprintf("%R/timeline?u=%h&c=%t&y=a&vfx", zDispUser, zDate); + } + cgi_printf("user: %z%h", href("%z",zLink), zDispUser); + }else{ + cgi_printf("user: %h", zDispUser); + } + + /* Generate the "tags: TAGLIST" at the end of the comment, together + ** with hyperlinks to the tag list. + */ + if( zTagList && zTagList[0]==0 ) zTagList = 0; + if( zTagList ){ + if( g.perm.Hyperlink ){ + int i; + const char *z = zTagList; + Blob links; + blob_zero(&links); + while( z && z[0] ){ + for(i=0; z[i] && (z[i]!=',' || z[i+1]!=' '); i++){} + if( zThisTag==0 || memcmp(z, zThisTag, i)!=0 || zThisTag[i]!=0 ){ + blob_appendf(&links, + "%z%#h%.2s", + href("%R/timeline?r=%#t&c=%t",i,z,zDate), i,z, &z[i] + ); + }else{ + blob_appendf(&links, "%#h", i+2, z); + } + if( z[i]==0 ) break; + z += i+2; + } + cgi_printf(" tags: %s", blob_str(&links)); + blob_reset(&links); + }else{ + cgi_printf(" tags: %h", zTagList); + } + } + + if( tmFlags & TIMELINE_SHOWRID ){ + int srcId = delta_source_rid(rid); + if( srcId ){ + cgi_printf(" id: %z%d←%d", + href("%R/deltachain/%d",rid), rid, srcId); + }else{ + cgi_printf(" id: %z%d", + href("%R/deltachain/%d",rid), rid); + } + } + tag_private_status(rid); + + if( (tmFlags & TIMELINE_SIMPLE)!=0 ){ + cgi_printf(""); /* End of the declutter span */ + } + + /* End timelineDetail */ + if( (tmFlags & TIMELINE_INLINE)!=0 ){ + cgi_printf(")"); + } +} + +/* +** Clients of the www_print_timeline() routine can create an instance +** of the following object to pass supplemental information into +** www_print_timeline() as the last (8th) argument. +*/ +#if INTERFACE +struct TimelineXtra { + void (*xExtra)(Stmt*,int,const char*,const char*); /* generate "extra" text */ + const char *zThisUser; /* Suppress links to this user */ + const char *zThisTag; /* Suppress links to this tag */ + Matcher *pLeftBranch; /* Comparison function to use for zLeftBranch */ + int selectedRid; /* CSS: timelineSelected */ + int secondRid; /* CSS: timelineSelected timelineSecondary */ + int currentRid; /* CSS: timelineCurrent */ +}; +#endif + + +/* +** SETTING: timeline-truncate-at-blank boolean default=off +** +** If enabled, check-in comments displayed on the timeline are truncated +** at the first blank line of the comment text. The comment text after +** the first blank line is only seen in the /info or similar pages that +** show details about the check-in. +*/ +/* +** SETTING: timeline-tslink-info boolean default=off +** +** The hyperlink on the timestamp associated with each timeline entry, +** on the far left-hand side of the screen, normally targets another +** /timeline page that shows the entry in context. However, if this +** option is turned on, that hyperlink targets the /info page showing +** the details of the entry. +*/ +/* +** SETTING: timeline-mark-leaves width=5 default=1 +** +** Determine whether or not leaf check-ins are marked as such in the +** details section of the timeline. The value is an integer between 0 +** and 2: +** +** 0 Do not show any special marking for leaf check-ins. +** +** 1 Show just "leaf" or "closed" +** +** 2 Show "Leaf" or "Closed-Leaf" with emphasis +** +** The default is currently 1. Prior to 2025-10-19, the default was 2. +** This setting has no effect on the "Classic" view, which always behaves +** as if the setting were 2. +*/ + +/* +** Output a timeline in the web format given a query. The query +** should return these columns: +** +** 0. rid +** 1. artifact hash +** 2. Date/Time +** 3. Comment string +** 4. User +** 5. True if is a leaf +** 6. background color +** 7. type ("ci", "w", "t", "e", "g", "f", "div") +** 8. list of symbolic tags. +** 9. tagid for ticket or wiki or event +** 10. Short comment to user for repeated tickets and wiki +*/ +void www_print_timeline( + Stmt *pQuery, /* Query to implement the timeline */ + int tmFlags, /* Flags controlling display behavior */ + TimelineXtra *pXtra /* Supplemental information */ +){ + int mxWikiLen; + Blob comment; + int prevTagid = 0; + int suppressCnt = 0; + char zPrevDate[20]; + GraphContext *pGraph = 0; + int prevWasDivider = 0; /* True if previous output row was
*/ + int fchngQueryInit = 0; /* True if fchngQuery is initialized */ + Stmt fchngQuery; /* Query for file changes on check-ins */ + int pendingEndTr = 0; /* True if a is needed */ + int vid = 0; /* Current check-out version */ + int dateFormat = 0; /* 0: HH:MM (default) */ + int bCommentGitStyle = 0; /* Only show comments through first blank line */ + const char *zStyle; /* Sub-name for classes for the style */ + const char *zDateFmt; + int iTableId = timeline_tableid(); + int bTimestampLinksToInfo; /* True if timestamp hyperlinks go to the /info + ** page rather than the /timeline page */ + const char *zMainBranch = db_main_branch(); + TimelineXtra zeroXtra; /* Substitute for pXtra==NULL */ + + if( pXtra==0 ){ + memset(&zeroXtra, 0, sizeof(zeroXtra)); + pXtra = &zeroXtra; + } + if( pXtra->currentRid ){ + vid = pXtra->currentRid; + }else if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){ + vid = db_lget_int("checkout", 0); + } + zPrevDate[0] = 0; + mxWikiLen = db_get_int("timeline-max-comment", 0); + dateFormat = db_get_int("timeline-date-format", 0); + bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0); + bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0); + if( (tmFlags & TIMELINE_VIEWS)==0 ){ + tmFlags |= timeline_ss_cookie(); + } + if( tmFlags & TIMELINE_COLUMNAR ){ + zStyle = "Columnar"; + }else if( tmFlags & TIMELINE_COMPACT ){ + zStyle = "Compact"; + }else if( tmFlags & TIMELINE_SIMPLE ){ + zStyle = "Simple"; + }else if( tmFlags & TIMELINE_VERBOSE ){ + zStyle = "Verbose"; + }else if( tmFlags & TIMELINE_CLASSIC ){ + zStyle = "Classic"; + }else{ + zStyle = "Modern"; + } + zDateFmt = P("datefmt"); + if( zDateFmt ) dateFormat = atoi(zDateFmt); + pGraph = graph_init(); + if( (tmFlags & TIMELINE_CHPICK)!=0 + && !db_table_exists("repository","cherrypick") + ){ + tmFlags &= ~TIMELINE_CHPICK; + } + @ \ + @ + blob_zero(&comment); + while( db_step(pQuery)==SQLITE_ROW ){ + int rid = db_column_int(pQuery, 0); + const char *zUuid = db_column_text(pQuery, 1); + int isLeaf = db_column_int(pQuery, 5); + const char *zBgClr = db_column_text(pQuery, 6); + const char *zDate = db_column_text(pQuery, 2); + const char *zType = db_column_text(pQuery, 7); + const char *zUser = db_column_text(pQuery, 4); + int tagid = db_column_int(pQuery, 9); + char *zBr = 0; /* Branch */ + int commentColumn = 3; /* Column containing comment text */ + int modPending; /* Pending moderation */ + char *zDateLink; /* URL for the link on the timestamp */ + int drawDetailEllipsis; /* True to show ellipsis in place of detail */ + int gidx = 0; /* Graph row identifier */ + int omitCommentColor = 0; /* True to skip added color to comments */ + const char *zExtraClass = ""; + char zTime[20]; + + if( zDate==0 ){ + zDate = "YYYY-MM-DD HH:MM:SS"; /* Something wrong with the repo */ + } + modPending = moderation_pending(rid); + if( tagid ){ + if( modPending ) tagid = -tagid; + if( tagid==prevTagid ){ + if( tmFlags & TIMELINE_BRIEF ){ + suppressCnt++; + continue; + }else{ + commentColumn = 10; + } + } + } + prevTagid = tagid; + if( suppressCnt ){ + @ ... %d(suppressCnt) similar + @ event%s(suppressCnt>1?"s":"") omitted. + suppressCnt = 0; + } + if( pendingEndTr ){ + @ + pendingEndTr = 0; + } + if( fossil_strcmp(zType,"div")==0 ){ + if( !prevWasDivider ){ + @ + } + prevWasDivider = 1; + continue; + } + prevWasDivider = 0; + /* Date format codes: + ** (0) HH:MM + ** (1) HH:MM:SS + ** (2) YYYY-MM-DD HH:MM + ** (3) YYMMDD HH:MM + ** (4) (off) + */ + if( dateFormat<2 ){ + if( fossil_strnicmp(zDate, zPrevDate, 10) ){ + sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate); + @ + } + memcpy(zTime, &zDate[11], 5+dateFormat*3); + zTime[5+dateFormat*3] = 0; + }else if( 2==dateFormat ){ + /* YYYY-MM-DD HH:MM */ + sqlite3_snprintf(sizeof(zTime), zTime, "%.16s", zDate); + }else if( 3==dateFormat ){ + /* YYMMDD HH:MM */ + int pos = 0; + zTime[pos++] = zDate[2]; zTime[pos++] = zDate[3]; /* YY */ + zTime[pos++] = zDate[5]; zTime[pos++] = zDate[6]; /* MM */ + zTime[pos++] = zDate[8]; zTime[pos++] = zDate[9]; /* DD */ + zTime[pos++] = ' '; + zTime[pos++] = zDate[11]; zTime[pos++] = zDate[12]; /* HH */ + zTime[pos++] = ':'; + zTime[pos++] = zDate[14]; zTime[pos++] = zDate[15]; /* MM */ + zTime[pos++] = 0; + }else{ + zTime[0] = 0; + } + pendingEndTr = 1; + if( rid==pXtra->selectedRid ){ + @ + omitCommentColor = 1; + }else if( rid==pXtra->secondRid ){ + @ + omitCommentColor = 1; + }else if( rid==vid ){ + @ + }else { + @ + } + if( zType[0]=='t' && tagid && (tmFlags & TIMELINE_NOTKT)==0 ){ + char *zTktid = db_text(0, "SELECT substr(tagname,5) FROM tag" + " WHERE tagid=%d", tagid); + if( zTktid ){ + int isClosed = 0; + if( is_ticket(zTktid, &isClosed) && isClosed ){ + zExtraClass = " tktTlClosed"; + }else{ + zExtraClass = " tktTlOpen"; + } + fossil_free(zTktid); + } + } + if( zType[0]=='e' && tagid ){ + if( bTimestampLinksToInfo ){ + char *zId; + zId = db_text(0, "SELECT substr(tagname, 7) FROM tag WHERE tagid=%d", + tagid); + zDateLink = href("%R/technote/%s",zId); + free(zId); + }else{ + zDateLink = href("%R/timeline?c=%t&y=a",zDate); + } + }else if( zUuid ){ + if( bTimestampLinksToInfo ){ + zDateLink = chref("timelineHistLink", "%R/info/%!S", zUuid); + }else{ + zDateLink = chref("timelineHistLink", "%R/timeline?c=%!S&y=a", zUuid); + } + }else{ + zDateLink = mprintf(""); + } + @ + @ + if( omitCommentColor ){ + @ + } + if( pGraph ){ + graph_finish(pGraph, pXtra->pLeftBranch, tmFlags); + if( pGraph->nErr ){ + graph_free(pGraph); + pGraph = 0; + }else{ + @ \ + @ + } + } + @

+ @
%s(zPrevDate)
+ @
%z(zDateLink)%s(zTime) + if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ + /* Don't use the requested background color. Use the background color + ** override from query parameters instead. */ + if( tmFlags & TIMELINE_UCOLOR ){ + zBgClr = zUser ? user_color(zUser) : 0; + }else if( tmFlags & TIMELINE_NOCOLOR ){ + zBgClr = 0; + }else if( zType[0]=='c' ){ + static Stmt qdelta; + db_static_prepare(&qdelta, "SELECT baseid IS NULL FROM plink" + " WHERE cid=:rid"); + db_bind_int(&qdelta, ":rid", rid); + if( db_step(&qdelta)!=SQLITE_ROW ){ + zBgClr = 0; /* Not a check-in */ + }else if( db_column_int(&qdelta, 0) ){ + zBgClr = hash_color("b"); /* baseline manifest */ + }else{ + zBgClr = hash_color("f"); /* delta manifest */ + } + db_reset(&qdelta); + } + }else{ + /* Make sure the user-specified background color is reasonable */ + zBgClr = reasonable_bg_color(zBgClr, 0); + } + if( zType[0]=='c' + && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0) + ){ + zBr = branch_of_rid(rid); + if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){ + /* If no background color is specified, use a color based on the + ** branch name */ + if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ + }else if( zBr==0 || strcmp(zBr, zMainBranch)==0 ){ + zBgClr = 0; + }else{ + zBgClr = hash_color(zBr); + } + } + } + if( zType[0]=='c' && pGraph ){ + int nParent = 0; + int nCherrypick = 0; + GraphRowId aParent[GR_MAX_RAIL]; + static Stmt qparent; + if( tmFlags & TIMELINE_GRAPH ){ + db_static_prepare(&qparent, + "SELECT pid FROM plink" + " WHERE cid=:rid AND pid NOT IN phantom" + " ORDER BY isprim DESC /*sort*/" + ); + db_bind_int(&qparent, ":rid", rid); + while( db_step(&qparent)==SQLITE_ROW && nParent0 ){ + static Stmt qcherrypick; + db_static_prepare(&qcherrypick, + "SELECT parentid FROM cherrypick" + " WHERE childid=:rid AND parentid NOT IN phantom" + ); + db_bind_int(&qcherrypick, ":rid", rid); + while( db_step(&qcherrypick)==SQLITE_ROW && nParent + }else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){ + /* For technotes, make a graph node with nParent==(-1). This will + ** not actually draw anything on the graph, but it will set the + ** background color of the timeline entry */ + gidx = graph_add_row(pGraph, rid, -1, 0, 0, zBr, zBgClr, zUuid, 0); + @
+ } + fossil_free(zBr); + @
+ }else{ + @ + } + if( pGraph ){ + if( zType[0]=='e' ){ + @ Note: + }else if( zType[0]!='c' ){ + @ • + } + } + if( modPending ){ + @ (Awaiting Moderator Approval) + } + if( (tmFlags & TIMELINE_BISECT)!=0 && zType[0]=='c' ){ + static Stmt bisectQuery; + db_static_prepare(&bisectQuery, + "SELECT seq, stat FROM bilog WHERE rid=:rid AND seq"); + db_bind_int(&bisectQuery, ":rid", rid); + if( db_step(&bisectQuery)==SQLITE_ROW ){ + @ %s(db_column_text(&bisectQuery,1)) + @ (%d(db_column_int(&bisectQuery,0))) + } + db_reset(&bisectQuery); + } + drawDetailEllipsis = (tmFlags & (TIMELINE_COMPACT))!=0; + db_column_blob(pQuery, commentColumn, &comment); + if( tmFlags & TIMELINE_COMPACT ){ + @ + }else{ + @ + } + if( (tmFlags & TIMELINE_CLASSIC)!=0 ){ + if( zType[0]=='c' ){ + hyperlink_to_version(zUuid); + if( isLeaf ){ + if( has_closed_tag(rid) ){ + @ Closed-Leaf: + }else{ + @ Leaf: + } + } + }else if( zType[0]=='e' && tagid ){ + hyperlink_to_event_tagid(tagid<0?-tagid:tagid); + }else if( (tmFlags & TIMELINE_ARTID)!=0 ){ + hyperlink_to_version(zUuid); + } + if( tmFlags & TIMELINE_SHOWRID ){ + int srcId = delta_source_rid(rid); + if( srcId ){ + @ (%z(href("%R/deltachain/%d",rid))%d(rid)←%d(srcId)) + }else{ + @ (%z(href("%R/deltachain/%d",rid))%d(rid)) + } + } + } + if( zType[0]!='c' ){ + /* Comments for anything other than a check-in are generated by + ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */ + if( zType[0]=='w' ){ + const char *zCom = blob_str(&comment); + /* Except, the comments generated by "fossil rebuild" for a wiki + ** page edit consist of a single character '-', '+', or ':' (to + ** indicate "deleted", "added", or "edited") followed by the + ** raw wiki page name. We have to generate an appropriate + ** comment on-the-fly + */ + wiki_hyperlink_override(zUuid); + if( zCom[0]=='-' ){ + @ Deleted wiki page "%z(href("%R/whistory?name=%t",zCom+1))\ + @ %h(zCom+1)" + }else if( (tmFlags & TIMELINE_REFS)!=0 + && (zCom[0]=='+' || zCom[0]==':') ){ + @ Wiki page "%z(href("%R/wiki?name=%t",zCom+1))%h(zCom+1)" + }else if( zCom[0]=='+' ){ + @ Added wiki page "%z(href("%R/wiki?name=%t",zCom+1))%h(zCom+1)" + }else if( zCom[0]==':' ){ + @ %z(href("%R/wdiff?id=%!S",zUuid))Changes to wiki page + @ "%z(href("%R/wiki?name=%t",zCom+1))%h(zCom+1)" + }else{ + /* Assume this is an attachment message. It _might_ also + ** be a legacy-format wiki log entry, in which case it + ** will simply be rendered in the older format. */ + wiki_convert(&comment, 0, WIKI_INLINE); + } + wiki_hyperlink_override(0); + }else{ + if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){ + blob_truncate_utf8(&comment, mxWikiLen); + blob_append(&comment, "...", 3); + } + wiki_convert(&comment, 0, WIKI_INLINE); + } + }else{ + if( bCommentGitStyle ){ + /* Truncate comment at first blank line */ + int ii, jj; + int n = blob_size(&comment); + char *z = blob_str(&comment); + for(ii=0; ii0 && (int)blob_size(&comment)>mxWikiLen ){ + blob_truncate_utf8(&comment, mxWikiLen); + blob_append(&comment, "...", 3); + @ %W(blob_str(&comment)) + drawDetailEllipsis = 0; + }else{ + cgi_printf("%W",blob_str(&comment)); + } + } + + @ + blob_reset(&comment); + + /* Generate extra information and hyperlinks that follow the comment. + ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)" + */ + if( drawDetailEllipsis ){ + @ ... + } + if( tmFlags & TIMELINE_COLUMNAR ){ + if( omitCommentColor ){ + @ + }else{ + @ + } + } + if( tmFlags & TIMELINE_COMPACT ){ + cgi_printf("",rid); + } + cgi_printf("", zStyle); + if( pXtra->xExtra ){ + pXtra->xExtra(pQuery, tmFlags, pXtra->zThisUser, pXtra->zThisTag); + }else{ + timeline_extra(pQuery, tmFlags, pXtra->zThisUser, pXtra->zThisTag); + } + if( tmFlags & TIMELINE_COMPACT ){ + @ + }else{ + @ + } + + /* Generate the file-change list if requested */ + if( (tmFlags & (TIMELINE_FCHANGES|TIMELINE_FRENAMES))!=0 + && zType[0]=='c' && g.perm.Hyperlink + ){ + int inUl = 0; + if( !fchngQueryInit ){ + db_prepare(&fchngQuery, + "SELECT pid," + " fid," + " (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name," + " (SELECT uuid FROM blob WHERE rid=fid)," + " (SELECT uuid FROM blob WHERE rid=pid)," + " (SELECT name FROM filename WHERE fnid=mlink.pfnid) AS oldnm" + " FROM mlink" + " WHERE mid=:mid AND (pid!=fid OR pfnid>0)" + " AND (fid>0 OR" + " fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=:mid))" + " AND NOT mlink.isaux" + " ORDER BY 3 /*sort*/" + ); + fchngQueryInit = 1; + } + db_bind_int(&fchngQuery, ":mid", rid); + while( db_step(&fchngQuery)==SQLITE_ROW ){ + const char *zFilename = db_column_text(&fchngQuery, 2); + int isNew = db_column_int(&fchngQuery, 0)<=0; + int isMergeNew = db_column_int(&fchngQuery, 0)<0; + int fid = db_column_int(&fchngQuery, 1); + int isDel = fid==0; + const char *zOldName = db_column_text(&fchngQuery, 5); + const char *zOld = db_column_text(&fchngQuery, 4); + const char *zNew = db_column_text(&fchngQuery, 3); + const char *zUnpub = ""; + char *zA; + char *zId; + if( !inUl ){ + @
    + inUl = 1; + } + if( tmFlags & TIMELINE_SHOWRID ){ + int srcId = delta_source_rid(fid); + if( srcId ){ + zId = mprintf(" (%z%d←%d) ", + href("%R/deltachain/%d", fid), fid, srcId); + }else{ + zId = mprintf(" (%z%d) ", + href("%R/deltachain/%d", fid), fid); + } + }else{ + zId = fossil_strdup(""); + } + if( (tmFlags & TIMELINE_FRENAMES)!=0 ){ + if( !isNew && !isDel && zOldName!=0 ){ + @
  • %h(zOldName) → %h(zFilename)%s(zId) + } + continue; + } + zA = href("%R/artifact/%!S",fid?zNew:zOld); + if( content_is_private(fid) ){ + zUnpub = UNPUB_TAG; + } + if( isNew ){ + @
  • %s(zA)%h(zFilename)%s(zId) %s(zUnpub) + if( isMergeNew ){ + @ (added by merge) + }else{ + @ (new file) + } + @   %z(href("%R/artifact/%!S",zNew))[view]
  • + }else if( isDel ){ + @
  • %s(zA)%h(zFilename) (deleted)
  • + }else if( fossil_strcmp(zOld,zNew)==0 && zOldName!=0 ){ + @
  • %h(zOldName) → %s(zA)%h(zFilename)%s(zId) + @ %s(zUnpub) %z(href("%R/artifact/%!S",zNew))[view]
  • + }else{ + if( zOldName!=0 ){ + @
  • %h(zOldName) → %s(zA)%h(zFilename)%s(zId) %s(zUnpub) + }else{ + @
  • %s(zA)%h(zFilename)%s(zId)   %s(zUnpub) + } + @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]
  • + } + fossil_free(zA); + fossil_free(zId); + } + db_reset(&fchngQuery); + if( inUl ){ + @
+ } + } + + /* Show the complete text of forum messages */ + if( (tmFlags & (TIMELINE_FORUMTXT))!=0 + && zType[0]=='f' && g.perm.Hyperlink + && (!content_is_private(rid) || g.perm.ModForum) + ){ + Manifest *pPost = manifest_get(rid, CFTYPE_FORUM, 0); + if( pPost ){ + const char *zClass = "forumTimeline"; + if( forum_rid_has_been_edited(rid) ){ + zClass = "forumTimeline forumObs"; + } + forum_render(0, pPost->zMimetype, pPost->zWiki, zClass, 1); + manifest_destroy(pPost); + } + } + } + if( suppressCnt ){ + @ ... %d(suppressCnt) similar + @ event%s(suppressCnt>1?"s":"") omitted. + suppressCnt = 0; + } + if( pendingEndTr ){ + @
+ if( fchngQueryInit ) db_finalize(&fchngQuery); + timeline_output_graph_javascript(pGraph, tmFlags, iTableId); +} + +/* +** Change the RGB background color given in the argument in a foreground +** color with the same hue. +*/ +static const char *bg_to_fg(const char *zIn){ + int i; + unsigned int x[3]; + unsigned int mx = 0; + static int whiteFg = -1; + static char zRes[10]; + if( strlen(zIn)!=7 || zIn[0]!='#' ) return zIn; + zIn++; + for(i=0; i<3; i++){ + x[i] = hex_digit_value(zIn[0])*16 + hex_digit_value(zIn[1]); + zIn += 2; + if( x[i]>mx ) mx = x[i]; + } + if( whiteFg<0 ) whiteFg = skin_detail_boolean("white-foreground"); + if( whiteFg ){ + /* Make the color lighter */ + static const unsigned int t = 215; + if( mxt ){ + for(i=0; i<3; i++){ + x[i] = x[i]>=mx-t ? x[i] - (mx-t) : 0; + } + } + } + sqlite3_snprintf(sizeof(zRes),zRes,"#%02x%02x%02x",x[0],x[1],x[2]); + return zRes; +} + +/* +** Generate all of the necessary javascript to generate a timeline +** graph. +*/ +void timeline_output_graph_javascript( + GraphContext *pGraph, /* The graph to be displayed */ + int tmFlags, /* Flags that control rendering */ + int iTableId /* Which graph is this for */ +){ + if( pGraph && pGraph->nErr==0 ){ + GraphRow *pRow; + int i; + char cSep; + int iRailPitch; /* Pixels between consecutive rails */ + int showArrowheads; /* True to draw arrowheads. False to omit. */ + int circleNodes; /* True for circle nodes. False for square nodes */ + int colorGraph; /* Use colors for graph lines */ + int iTopRow; /* Index of the top row of the graph */ + int fileDiff; /* True for file diff. False for check-in diff */ + int omitDescenders; /* True to omit descenders */ + int scrollToSelect; /* True to scroll to the selection */ + int dwellTimeout; /* Milliseconds to wait for tooltips to show */ + int closeTimeout; /* Milliseconds to wait for tooltips to close */ + u8 *aiMap; /* The rail map */ + u8 bNoGraph; /* True to show a minimal graph */ + + bNoGraph = (tmFlags & TIMELINE_GRAPH)==0; + iRailPitch = atoi(PD("railpitch","0")); + showArrowheads = skin_detail_boolean("timeline-arrowheads"); + circleNodes = skin_detail_boolean("timeline-circle-nodes"); + colorGraph = skin_detail_boolean("timeline-color-graph-lines"); + iTopRow = pGraph->pFirst ? pGraph->pFirst->idx : 0; + omitDescenders = (tmFlags & TIMELINE_DISJOINT)!=0; + fileDiff = (tmFlags & TIMELINE_FILEDIFF)!=0; + scrollToSelect = (tmFlags & TIMELINE_NOSCROLL)==0; + dwellTimeout = atoi(db_get("timeline-dwelltime","100")); + closeTimeout = atoi(db_get("timeline-closetime","250")); + @ + builtin_request_js("graph.js"); + builtin_request_js("copybtn.js"); /* Required by graph.js */ + graph_free(pGraph); + } +} + +/* +** Create a temporary table suitable for storing timeline data. +*/ +static void timeline_temp_table(void){ + static const char zSql[] = + @ CREATE TEMP TABLE IF NOT EXISTS timeline( + @ rid INTEGER PRIMARY KEY, + @ uuid TEXT, + @ timestamp TEXT, + @ comment TEXT, + @ user TEXT, + @ isleaf BOOLEAN, + @ bgcolor TEXT, + @ etype TEXT, + @ taglist TEXT, + @ tagid INTEGER, + @ short TEXT, + @ sortby REAL + @ ) + ; + db_multi_exec("%s", zSql/*safe-for-%s*/); +} + +/* +** Return a pointer to a constant string that forms the basis +** for a timeline query for the WWW interface. +*/ +const char *timeline_query_for_www(void){ + static const char zBase[] = + @ SELECT + @ blob.rid AS blobRid, + @ uuid AS uuid, + @ datetime(event.mtime,toLocal()) AS timestamp, + @ coalesce(ecomment, comment) AS comment, + @ coalesce(euser, user) AS user, + @ blob.rid IN leaf AS leaf, + @ bgcolor AS bgColor, + @ event.type AS eventType, + @ (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref + @ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid + @ AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags, + @ tagid AS tagid, + @ brief AS brief, + @ event.mtime AS mtime + @ FROM event CROSS JOIN blob + @ WHERE blob.rid=event.objid + ; + return zBase; +} + +/* +** zDate is a localtime date. Insert records into the +** "timeline" table to cause
to be inserted on zDate. +*/ +static int timeline_add_divider(double rDate){ + int rid = db_int(-1, + "SELECT rid FROM timeline ORDER BY abs(sortby-%.16g) LIMIT 1", rDate + ); + if( rid>0 ) return rid; + db_multi_exec( + "INSERT INTO timeline(rid,sortby,etype) VALUES(-1,%.16g,'div')", + rDate + ); + return -1; +} + +/* +** Return all possible names for file zUuid. +*/ +char *names_of_file(const char *zUuid){ + Stmt q; + Blob out; + const char *zSep = ""; + db_prepare(&q, + "SELECT DISTINCT filename.name FROM mlink, filename" + " WHERE mlink.fid=(SELECT rid FROM blob WHERE uuid=%Q)" + " AND filename.fnid=mlink.fnid", + zUuid + ); + blob_zero(&out); + while( db_step(&q)==SQLITE_ROW ){ + const char *zFN = db_column_text(&q, 0); + blob_appendf(&out, "%s%z%h
", zSep, + href("%R/finfo?name=%t&m=%!S", zFN, zUuid), zFN); + zSep = " or "; + } + db_finalize(&q); + return blob_str(&out); +} + + +/* +** Add the select/option box to the timeline submenu that is used to +** set the y= parameter that determines which elements to display +** on the timeline. +*/ +static void timeline_y_submenu(int isDisabled){ + static int i = 0; + static const char *az[20]; + if( i==0 ){ + az[0] = "all"; + az[1] = "Any Type"; + i = 2; + if( g.perm.Read ){ + az[i++] = "ci"; + az[i++] = "Check-ins"; + az[i++] = "g"; + az[i++] = "Tags"; + } + if( g.perm.RdWiki ){ + az[i++] = "e"; + az[i++] = "Tech Notes"; + } + if( g.perm.RdTkt ){ + az[i++] = "t"; + az[i++] = "Tickets"; + az[i++] = "n"; + az[i++] = "New Tickets"; + } + if( g.perm.RdWiki ){ + az[i++] = "w"; + az[i++] = "Wiki"; + } + if( g.perm.RdForum ){ + az[i++] = "f"; + az[i++] = "Forum"; + az[i++] = "h"; + az[i++] = "New Threads"; + } + assert( i<=count(az) ); + } + if( i>2 ){ + style_submenu_multichoice("y", i/2, az, isDisabled); + } +} + +/* +** SETTING: timeline-default-style width=5 default=m +** +** This setting determines the default "view style" for timelines. +** The setting should be a single character, one of the following: +** +** c Compact +** j Columnar +** m Modern +** s Simple +** v Verbose +** x Classic +** +** The default value is m (Modern). +*/ + +/* +** Return the default value for the "ss" cookie or query parameter. +** The "ss" cookie determines the graph style. See the +** timeline_view_styles[] global constant for a list of choices. +*/ +const char *timeline_default_ss(void){ + static const char *zSs = 0; + if( zSs==0 ) zSs = db_get("timeline-default-style","m"); + return zSs; +} + +/* +** Convert the current "ss" display preferences cookie into an +** appropriate TIMELINE_* flag +*/ +int timeline_ss_cookie(void){ + int tmFlags; + const char *v = cookie_value("ss",0); + if( v==0 ) v = timeline_default_ss(); + switch( v[0] ){ + case 'c': tmFlags = TIMELINE_COMPACT; break; + case 'v': tmFlags = TIMELINE_VERBOSE; break; + case 'j': tmFlags = TIMELINE_COLUMNAR; break; + case 'x': tmFlags = TIMELINE_CLASSIC; break; + case 's': tmFlags = TIMELINE_SIMPLE; break; + default: tmFlags = TIMELINE_MODERN; break; + } + return tmFlags; +} + +/* Available timeline display styles, together with their y= query +** parameter names. +*/ +const char *const timeline_view_styles[] = { + "m", "Modern View", + "j", "Columnar View", + "c", "Compact View", + "s", "Simple View", + "v", "Verbose View", + "x", "Classic View", +}; +#if INTERFACE +# define N_TIMELINE_VIEW_STYLE 6 +#endif + +/* +** Add the select/option box to the timeline submenu that is used to +** set the ss= parameter that determines the viewing mode. +** +** Return the TIMELINE_* value appropriate for the view-style. +*/ +int timeline_ss_submenu(void){ + cookie_link_parameter("ss","ss",timeline_default_ss()); + style_submenu_multichoice("ss", + N_TIMELINE_VIEW_STYLE, + timeline_view_styles, 0); + return timeline_ss_cookie(); +} + +/* +** If the zChng string is not NULL, then it should be a comma-separated +** list of glob patterns for filenames. Add an term to the WHERE clause +** for the SQL statement under construction that excludes any check-in that +** does not modify one or more files matching the globs. +*/ +static void addFileGlobExclusion( + const char *zChng, /* The filename GLOB list */ + Blob *pSql /* The SELECT statement under construction */ +){ + if( zChng==0 ) return; + blob_append_sql(pSql," AND event.objid IN (" + "SELECT mlink.mid FROM mlink, filename\n" + " WHERE mlink.fnid=filename.fnid\n" + " AND %s)", + glob_expr("filename.name", mprintf("\"%s\"", zChng))); +} +static void addFileGlobDescription( + const char *zChng, /* The filename GLOB list */ + Blob *pDescription /* Result description */ +){ + if( zChng==0 ) return; + blob_appendf(pDescription, " that include changes to files matching '%h'", + zChng); +} + +/* +** If zChng is not NULL, then use it as a comma-separated list of +** glob patterns for filenames, and remove from the "ok" table any +** check-ins that do not modify one or more of the files identified +** by zChng. +*/ +static void removeFileGlobFromOk( + const char *zChng /* The filename GLOB list */ +){ + if( zChng==0 ) return; + db_multi_exec( + "DELETE FROM ok WHERE rid NOT IN (\n" + " SELECT mlink.mid FROM mlink, filename\n" + " WHERE mlink.fnid=filename.fnid\n" + " AND %z);\n", + glob_expr("filename.name", zChng) + ); +} + +/* +** Similar to fossil_expand_datetime() +** +** Add missing "-" characters into a date/time. Examples: +** +** 20190419 => 2019-04-19 +** 201904 => 2019-04 +*/ +const char *timeline_expand_datetime(const char *zIn, int *pbZulu){ + static char zEDate[16]; + int n = (int)strlen(zIn); + int i, j; + + /* These forms are recognized: + ** + ** (1) YYYYMMDD + ** (2) YYYYMM + ** (3) YYYYWW + */ + if( n && (zIn[n-1]=='Z' || zIn[n-1]=='z') ){ + n--; + if( pbZulu ) *pbZulu = 1; + }else{ + if( pbZulu ) *pbZulu = 0; + } + if( n!=8 && n!=6 ) return zIn; + + /* Every character must be a digit */ + for(i=0; i18 ) return 0; + if( n==18 ){ + if( zDay[17]!='Z' && zDay[17]!='z' ) return 0; + n--; + } + if( zDay[8]!='-' ) return 0; + for(i=0; i<17 && (fossil_isdigit(zDay[i]) || i==8); i++){} + if( i!=17 ) return 0; + i = atoi(zDay); + d = i%100; + if( d<1 || d>31 ) return 0; + m = (i/100)%100; + if( m<1 || m>12 ) return 0; + i = atoi(zDay+9); + d = i%100; + if( d<1 || d>31 ) return 0; + m = (i/100)%100; + if( m<1 || m>12 ) return 0; + return 1; +} + +/* +** Find the first check-in encountered with a particular tag +** when moving either forwards are backwards in time from a +** particular starting point (iFrom). Return the rid of that +** first check-in. If there are no check-ins in the descendant +** or ancestor set of check-in iFrom that match the tag, then +** return 0. +** +** If looking fowards (if bForward is true) then the first link +** is allowed to be a cherrypick, but subsequent links must be +** either full merges or normal check-ins. The idea is that we +** are looking for the first use of the code in the iFrom check-in +** that has the zEnd tag. The first link is allowed to be a cherrypick +** because the code in question crosses that link, but the code does +** not cross any subsequent cherrypick. +*/ +static int timeline_endpoint( + int iFrom, /* Starting point */ + const char *zEnd, /* Tag we are searching for */ + int bForward /* 1: forwards in time (descendants) 0: backwards */ +){ + int tagId; + int endId = 0; + Stmt q; + int ans = 0; + + tagId = db_int(0, "SELECT tagid FROM tag WHERE tagname='sym-%q'", zEnd); + if( tagId==0 ){ + endId = symbolic_name_to_rid(zEnd, "ci"); + if( endId==0 ) return 0; + } + db_pause_dml_log(); + if( bForward ){ + if( tagId ){ + db_prepare(&q, + "WITH RECURSIVE dx(id,mtime) AS (" + " SELECT %d, event.mtime FROM event WHERE objid=%d" + " UNION" + " SELECT cp.childid, event.mtime FROM cherrypick AS cp, event" + " WHERE cp.parentid=%d AND NOT cp.isExclude" + " AND event.objid=cp.childid" + " UNION" + " SELECT plink.cid, plink.mtime" + " FROM dx, plink" + " WHERE plink.pid=dx.id" + " AND plink.mtime<=(SELECT max(event.mtime) FROM tagxref, event" + " WHERE tagxref.tagid=%d AND tagxref.tagtype>0" + " AND event.objid=tagxref.rid)" + " ORDER BY plink.mtime)" + "SELECT id FROM dx, tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=id" + " ORDER BY dx.mtime LIMIT 1", + iFrom, iFrom, iFrom, tagId, tagId + ); + }else{ + db_prepare(&q, + "WITH RECURSIVE dx(id,mtime) AS (" + " SELECT %d, event.mtime FROM event WHERE objid=%d" + " UNION" + " SELECT cp.childid, event.mtime FROM cherrypick AS cp, event" + " WHERE cp.parentid=%d AND NOT cp.isExclude" + " AND event.objid=cp.childid" + " UNION" + " SELECT plink.cid, plink.mtime" + " FROM dx, plink" + " WHERE plink.pid=dx.id" + " AND plink.mtime<=(SELECT mtime FROM event WHERE objid=%d)" + " ORDER BY plink.mtime)" + "SELECT id FROM dx WHERE id=%d", + iFrom, iFrom, iFrom, endId, endId + ); + } + }else{ + if( tagId ){ + db_prepare(&q, + "WITH RECURSIVE dx(id,mtime) AS (" + " SELECT %d, event.mtime FROM event WHERE objid=%d" + " UNION" + " SELECT plink.pid, event.mtime" + " FROM dx, plink, event" + " WHERE plink.cid=dx.id AND event.objid=plink.pid" + " AND event.mtime>=(SELECT min(event.mtime) FROM tagxref, event" + " WHERE tagxref.tagid=%d AND tagxref.tagtype>0" + " AND event.objid=tagxref.rid)" + " ORDER BY event.mtime DESC)" + "SELECT id FROM dx, tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=id" + " ORDER BY dx.mtime DESC LIMIT 1", + iFrom, iFrom, tagId, tagId + ); + }else{ + db_prepare(&q, + "WITH RECURSIVE dx(id,mtime) AS (" + " SELECT %d, event.mtime FROM event WHERE objid=%d" + " UNION" + " SELECT plink.pid, event.mtime" + " FROM dx, plink, event" + " WHERE plink.cid=dx.id AND event.objid=plink.pid" + " AND event.mtime>=(SELECT mtime FROM event WHERE objid=%d)" + " ORDER BY event.mtime DESC)" + "SELECT id FROM dx WHERE id=%d", + iFrom, iFrom, endId, endId + ); + } + } + if( db_step(&q)==SQLITE_ROW ){ + ans = db_column_int(&q, 0); + } + db_finalize(&q); + db_unpause_dml_log(); + return ans; +} + +/* +** Add to the (temp) table zTab, RID values for every check-in +** identifier found on the zExtra string. Check-in names can be separated +** by commas or by whitespace. +*/ +static void add_extra_rids(const char *zTab, const char *zExtra){ + int ii; + int rid; + int cnt; + Blob sql; + char *zX; + char *zToDel; + if( zExtra==0 ) return; + cnt = 0; + blob_init(&sql, 0, 0); + zX = zToDel = fossil_strdup(zExtra); + blob_append_sql(&sql, "INSERT OR IGNORE INTO \"%w\" VALUES", zTab); + while( zX[0] ){ + char c; + if( zX[0]==',' || zX[0]==' ' ){ zX++; continue; } + for(ii=1; zX[ii] && zX[ii]!=',' && zX[ii]!=' '; ii++){} + c = zX[ii]; + zX[ii] = 0; + rid = name_to_rid(zX); + if( rid>0 ){ + if( (cnt%10)==4 ){ + blob_append_sql(&sql,",\n "); + }else if( cnt>0 ){ + blob_append_sql(&sql,","); + } + blob_append_sql(&sql, "(%d)", rid); + cnt++; + } + zX[ii] = c; + zX += ii; + } + if( cnt ) db_exec_sql(blob_sql_text(&sql)); + blob_reset(&sql); + fossil_free(zToDel); +} + +/* +** COMMAND: test-endpoint +** +** Usage: fossil test-endpoint BASE TAG ?OPTIONS? +** +** Show the first check-in with TAG that is a descendant or ancestor +** of BASE. The first descendant check-in is shown by default. Use +** the --backto to see the first ancestor check-in. +** +** Options: +** +** --backto Show ancestor. Others defaults to descendants. +*/ +void timeline_test_endpoint(void){ + int bForward = find_option("backto",0,0)==0; + int from_rid; + int ans; + db_find_and_open_repository(0, 0); + verify_all_options(); + if( g.argc!=4 ){ + usage("BASE-CHECKIN TAG ?--backto?"); + } + from_rid = symbolic_name_to_rid(g.argv[2],"ci"); + ans = timeline_endpoint(from_rid, g.argv[3], bForward); + if( ans ){ + fossil_print("Result: %d (%S)\n", ans, rid_to_uuid(ans)); + }else{ + fossil_print("No path found\n"); + } +} + + +/* +** WEBPAGE: timeline +** +** Query parameters: +** +** a=TIMEORTAG Show events after TIMEORTAG. +** b=TIMEORTAG Show events before TIMEORTAG. +** c=TIMEORTAG Show events that happen "circa" TIMEORTAG +** cf=FILEHASH Show events around the time of the first use of +** the file with FILEHASH. +** m=TIMEORTAG Highlight the event at TIMEORTAG, or the closest available +** event if TIMEORTAG is not part of the timeline. If +** the t= or r= is used, the m event is added to the timeline +** if it isn't there already. +** x=LIST Show check-ins in the comma- or space-separated LIST +** in addition to check-ins specified by other parameters. +** sel1=TIMEORTAG Highlight the check-in at TIMEORTAG if it is part of +** the timeline. Similar to m= except TIMEORTAG must +** match a check-in that is already in the timeline. +** sel2=TIMEORTAG Like sel1= but use the secondary highlight. +** n=COUNT Maximum number of events. "all" for no limit +** n1=COUNT Same as "n" but doesn't set the display-preference cookie +** Use "n1=COUNT" for a one-time display change +** p=CHECKIN Parents and ancestors of CHECKIN +** bt=PRIOR ... going back to PRIOR +** p2=CKIN2 ... use CKIN2 if CHECKIN is not found +** d=CHECKIN Children and descendants of CHECKIN +** d2=CKIN2 ... Use CKIN2 if CHECKIN is not found +** ft=DESCENDANT ... going forward to DESCENDANT +** dp=CHECKIN Same as 'd=CHECKIN&p=CHECKIN' +** dp2=CKIN2 Same as 'd2=CKIN2&p2=CKIN2' +** df=CHECKIN Same as 'd=CHECKIN&n1=all&nd'. Mnemonic: "Derived From" +** bt=CHECKIN "Back To". Show ancestors going back to CHECKIN +** p=CX ... from CX back to time of CHECKIN +** from=CX ... path from CX back to CHECKIN +** ft=CHECKIN "Forward To": Show descendants forward to CHECKIN +** d=CX ... from CX up to the time of CHECKIN +** from=CX ... path from CX up to CHECKIN +** t=TAG Show only check-ins with the given TAG +** r=TAG Same as 't=TAG&rel'. Mnemonic: "Related" +** tl=TAGLIST Same as 't=TAGLIST&ms=brlist'. Mnemonic: "Tag List" +** rl=TAGLIST Same as 'r=TAGLIST&ms=brlist'. Mnemonic: "Related List" +** ml=TAGLIST Same as 'tl=TAGLIST&mionly'. Mnemonic: "Merge-in List" +** sl=TAGLIST "Sort List". Draw TAGLIST branches ordered left to right. +** rel Show related check-ins as well as those matching t=TAG +** mionly Show related parents but not related children. +** nowiki Do not show wiki associated with branch or tag +** ms=MATCHSTYLE Set tag name match algorithm. One of "exact", "glob", +** "like", or "regexp". +** u=USER Only show items associated with USER +** y=TYPE 'ci', 'w', 't', 'n', 'e', 'f', 'h', or 'all'. +** ss=VIEWSTYLE c: "Compact", v: "Verbose", m: "Modern", j: "Columnar", +** x: "Classic". +** advm Use the "Advanced" or "Busy" menu design. +** ng No Graph. +** ncp Omit cherrypick merges +** nd Do not highlight the focus check-in +** nsm Omit the submenu +** nc Omit all graph colors other than highlights +** v Show details of files changed +** vfx Show complete text of forum messages +** f=CHECKIN Family (immediate parents and children) of CHECKIN +** from=CHECKIN Path through common ancestor from CHECKIN... +** to=CHECKIN ... to this +** to2=CHECKIN ... backup name if to= doesn't resolve +** shortest ... pick path with least number of nodes +** rel ... also show related checkins +** min ... hide long sequences along same branch +** bt=PRIOR ... path from CHECKIN back to PRIOR +** ft=LATER ... path from CHECKIN forward to LATER +** me=CHECKIN Most direct path from CHECKIN... +** you=CHECKIN ... to this +** rel ... also show related checkins +** uf=FILE_HASH Show only check-ins that contain the given file version +** All qualifying check-ins are shown unless there is +** also an n= or n1= query parameter. +** chng=GLOBLIST Show only check-ins that involve changes to a file whose +** name matches one of the comma-separated GLOBLIST +** brbg Background color determined by branch name +** ubg Background color determined by user +** deltabg Background color red for delta manifests or green +** for baseline manifests +** namechng Show only check-ins that have filename changes +** forks Show only forks and their children +** cherrypicks Show all cherrypicks +** ym=YYYYMM Show only events for the given year/month +** yw=YYYYWW Show only events for the given week of the given year +** yw=YYYYMMDD Show events for the week that includes the given day +** ymd=YYYYMMDD Show only events on the given day. The use "ymd=now" +** to see all changes for the current week. Add "z" at end +** to divide days at UTC instead of localtime days. +** Use ymd=YYYYMMDD-YYYYMMDD (with optional "z") for a range. +** year=YYYY Show only events on the given year. The use "year=0" +** to see all changes for the current year. +** days=N Show events over the previous N days +** datefmt=N Override the date format: 0=HH:MM, 1=HH:MM:SS, +** 2=YYYY-MM-DD HH:MM:SS, 3=YYMMDD HH:MM, and 4 means "off". +** bisect Show the check-ins that are in the current bisect +** oldestfirst Show events oldest first. +** showid Show RIDs +** showsql Show the SQL used to generate the report +** +** p= and d= can appear individually or together. If either p= or d= +** appear, then u=, y=, a=, and b= are ignored. +** +** If both a= and b= appear then both upper and lower bounds are honored. +** +** When multiple time-related filters are used, e.g. ym, yw, and ymd, +** which one(s) is/are applied is unspecified and may change between +** fossil versions. +** +** CHECKIN or TIMEORTAG can be a check-in hash prefix, or a tag, or the +** name of a branch. +*/ +void page_timeline(void){ + Stmt q; /* Query used to generate the timeline */ + Blob sql; /* text of SQL used to generate timeline */ + Blob desc; /* Description of the timeline */ + int nEntry; /* Max number of entries on timeline */ + int p_rid; /* artifact p and its parents */ + int d_rid; /* artifact d and descendants */ + int f_rid; /* artifact f and close family */ + const char *zUser = P("u"); /* All entries by this user if not NULL */ + const char *zType; /* Type of events to display */ + const char *zAfter = P("a"); /* Events after this time */ + const char *zBefore = P("b"); /* Events before this time */ + const char *zCirca = P("c"); /* Events near this time */ + const char *zMark = P("m"); /* Mark this event or an event this time */ + const char *zTagName = P("t"); /* Show events with this tag */ + const char *zBrName = P("r"); /* Equivalent to t=TAG&rel */ + int related = PB("rel"); /* Show events related to zTagName */ + const char *zMatchStyle = P("ms"); /* Tag/branch match style string */ + MatchStyle matchStyle = MS_EXACT; /* Match style code */ + const char *zMatchDesc = 0; /* Tag match expression description text */ + const char *zError = 0; /* Tag match error string */ + const char *zTagSql = 0; /* Tag/branch match SQL expression */ + const char *zSearch = P("s"); /* Search string */ + const char *zUses = P("uf"); /* Only show check-ins hold this file */ + const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ + const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ + char *zYearWeekStart = 0; /* YYYY-MM-DD for start of YYYY-WW */ + const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ + const char *zYear = P("year"); /* Events for the year YYYY */ + const char *zNDays = P("days"); /* Show events over the previous N days */ + int nDays = 0; /* Numeric value for zNDays */ + const char *zChng = P("chng"); /* List of GLOBs for files that changed */ + int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */ + int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */ + int forkOnly = PB("forks"); /* Show only forks and their children */ + int bisectLocal = PB("bisect"); /* Show the check-ins of the bisect */ + const char *zBisect = P("bid"); /* Bisect description */ + int cpOnly = PB("cherrypicks"); /* Show all cherrypick check-ins */ + int tmFlags = 0; /* Timeline flags */ + const char *zThisTag = 0; /* Suppress links to this tag */ + const char *zThisUser = 0; /* Suppress links to this user */ + HQuery url; /* URL for various branch links */ + int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */ + const char *zTo2 = 0; + int to_rid = name_choice("to","to2",&zTo2); /* to= for path timelines */ + int bShort = P("shortest")!=0; /* shortest possible path */ + int me_rid = name_to_typed_rid(P("me"),"ci"); /* me= for common ancestory */ + int you_rid = name_to_typed_rid(P("you"),"ci");/* you= for common ancst */ + int pd_rid; + const char *zDPNameP, *zDPNameD; /* Value of p=, d=, or dp= params */ + double rBefore, rAfter, rCirca; /* Boundary times */ + const char *z; + char *zOlderButton = 0; /* URL for Older button at the bottom */ + char *zOlderButtonLabel = 0; /* Label for the Older Button */ + char *zNewerButton = 0; /* URL for Newer button at the top */ + char *zNewerButtonLabel = 0; /* Label for the Newer button */ + int selectedRid = 0; /* Show a highlight on this RID */ + int secondaryRid = 0; /* Show secondary highlight */ + int disableY = 0; /* Disable type selector on submenu */ + int advancedMenu = 0; /* Use the advanced menu design */ + char *zPlural; /* Ending for plural forms */ + int showCherrypicks = 1; /* True to show cherrypick merges */ + int haveParameterN; /* True if n= query parameter present */ + int from_to_mode = 0; /* 0: from,to. 1: from,ft 2: from,bt */ + int showSql = PB("showsql"); /* True to show the SQL */ + Blob allSql; /* Copy of all SQL text */ + int bMin = PB("min"); /* True if "min" query parameter used */ + + login_check_credentials(); + url_initialize(&url, "timeline"); + cgi_query_parameters_to_url(&url); + blob_init(&allSql, 0, 0); + + /* The "mionly" query parameter is like "rel", but shows merge-ins only */ + if( P("mionly")!=0 ) related = 2; + + (void)P_NoBot("ss") + /* "ss" is processed via the udc but at least one spider likes to + ** try to SQL inject via this argument, so let's catch that. */; + + /* Set number of rows to display */ + z = P("n"); + if( z!=0 ){ + haveParameterN = 1; + cookie_write_parameter("n","n",0); + }else{ + const char *z2; + haveParameterN = 0; + cookie_read_parameter("n","n"); + z = P("n"); + if( z==0 ){ + z = db_get("timeline-default-length",0); + } + cgi_replace_query_parameter("n",fossil_strdup(z)); + cookie_write_parameter("n","n",0); + z2 = P("n1"); + if( z2 ){ + haveParameterN = 2; + z = z2; + } + } + if( z ){ + if( fossil_strcmp(z,"all")==0 ){ + nEntry = 0; + }else{ + nEntry = atoi(z); + if( nEntry<=0 ){ + z = "50"; + nEntry = 50; + } + } + }else{ + nEntry = 50; + } + + /* Query parameters d=, p=, and f= and variants */ + p_rid = name_choice("p","p2", &zDPNameP); + d_rid = name_choice("d","d2", &zDPNameD); + z = P("f"); + f_rid = z ? name_to_typed_rid(z,"ci") : 0; + z = P("df"); + if( z && (d_rid = name_to_typed_rid(z,"ci"))!=0 ){ + nEntry = 0; + useDividers = 0; + cgi_replace_query_parameter("d",fossil_strdup(z)); + zDPNameD = zDPNameP = z; + } + if( zChng && zChng[0]==0 ) zChng = 0; + + /* Undocumented query parameter to set JS mode */ + builtin_set_js_delivery_mode(P("jsmode"),1); + + secondaryRid = name_to_typed_rid(P("sel2"),"ci"); + selectedRid = name_to_typed_rid(P("sel1"),"ci"); + if( from_rid!=0 && to_rid!=0 ){ + if( selectedRid==0 ) selectedRid = from_rid; + if( secondaryRid==0 ) secondaryRid = to_rid; + } + tmFlags |= timeline_ss_submenu(); + cookie_link_parameter("advm","advm","0"); + advancedMenu = atoi(PD("advm","0")); + + /* Omit all cherry-pick merge lines if the "ncp" query parameter is + ** present or if this repository lacks a "cherrypick" table. */ + if( PB("ncp") || !db_table_exists("repository","cherrypick") ){ + showCherrypicks = 0; + } + + /* To view the timeline, must have permission to read project data. + */ + pd_rid = name_choice("dp","dp2",&zDPNameP); + if( pd_rid ){ + p_rid = d_rid = pd_rid; + zDPNameD = zDPNameP; + } + if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum) + || (bisectLocal && !g.perm.Setup) + ){ + login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); + return; + } + if( zBefore || zCirca ){ + if( robot_restrict("timelineX") ) return; + } + if( !bisectLocal ){ + etag_check(ETAG_QUERY|ETAG_COOKIE|ETAG_DATA|ETAG_CONFIG, 0); + } + cookie_read_parameter("y","y"); + zType = P("y"); + if( zType==0 ){ + zType = g.perm.Read ? "ci" : "all"; + cgi_set_parameter("y", zType); + } + if( zType[0]=='a' || + ( g.perm.Read && zType[0]=='c' ) || + ( g.perm.RdTkt && (zType[0]=='t' || zType[0]=='n') ) || + ( g.perm.RdWiki && (zType[0]=='w' || zType[0]=='e') ) || + ( g.perm.RdForum && zType[0]=='f' ) + ){ + cookie_write_parameter("y","y",zType); + } + + /* Convert the cf=FILEHASH query parameter into a c=CHECKINHASH value */ + if( P("cf")!=0 ){ + zCirca = db_text(0, + "SELECT (SELECT uuid FROM blob WHERE rid=mlink.mid)" + " FROM mlink, event" + " WHERE mlink.fid=(SELECT rid FROM blob WHERE uuid LIKE '%q%%')" + " AND event.objid=mlink.mid" + " ORDER BY event.mtime LIMIT 1", + P("cf") + ); + } + + /* Check for tl=TAGLIST and rl=TAGLIST which are abbreviations for + ** t=TAGLIST&ms=brlist and r=TAGLIST&ms=brlist repectively. */ + if( zBrName==0 && zTagName==0 ){ + const char *z; + const char *zPattern = 0; + if( (z = P("tl"))!=0 ){ + zPattern = zTagName = z; + }else if( (z = P("rl"))!=0 ){ + zPattern = zBrName = z; + if( related==0 ) related = 1; + }else if( (z = P("ml"))!=0 ){ + zPattern = zBrName = z; + if( related==0 ) related = 2; + } + if( zPattern!=0 && zMatchStyle==0 ){ + /* If there was no ms= query parameter, set the match style to + ** "glob" if the pattern appears to contain GLOB character, or + ** "brlist" if it does not. */ + if( strpbrk(zPattern,"*[?") ){ + zMatchStyle = "glob"; + }else{ + zMatchStyle = "brlist"; + } + } + } + + /* Convert r=TAG to t=TAG&rel in order to populate the UI style widgets. */ + if( zBrName ){ + cgi_delete_query_parameter("r"); + cgi_set_query_parameter("t", zBrName); (void)P("t"); + cgi_set_query_parameter("rel", "1"); + zTagName = zBrName; + if( related==0 ) related = 1; + zType = "ci"; + } + + /* Ignore empty tag query strings. */ + if( zTagName && !*zTagName ){ + zTagName = 0; + } + + /* Finish preliminary processing of tag match queries. */ + matchStyle = match_style(zMatchStyle, MS_EXACT); + if( zTagName ){ + zType = "ci"; + if( matchStyle==MS_EXACT ){ + /* For exact matching, inhibit links to the selected tag. */ + zThisTag = zTagName; + Th_StoreUnsafe("current_checkin", zTagName); + } + + /* Display a checkbox to enable/disable display of related check-ins. */ + if( advancedMenu ){ + style_submenu_checkbox("rel", "Related", 0, 0); + } + + /* Construct the tag match expression. */ + zTagSql = match_tag_sqlexpr(matchStyle, zTagName, &zMatchDesc, &zError); + } + + if( zMark && zMark[0]==0 ){ + if( zAfter ) zMark = zAfter; + if( zBefore ) zMark = zBefore; + if( zCirca ) zMark = zCirca; + } + if( (zTagSql && db_int(0,"SELECT count(*) " + "FROM tagxref NATURAL JOIN tag WHERE %s",zTagSql/*safe-for-%s*/)<=nEntry) + ){ + nEntry = -1; + zCirca = 0; + } + if( zType[0]=='a' ){ + tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK; + }else{ + tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK; + } + if( related ){ + tmFlags |= TIMELINE_FILLGAPS | TIMELINE_XMERGE; + tmFlags &= ~TIMELINE_DISJOINT; + } + if( PB("ncp") ){ + tmFlags &= ~TIMELINE_CHPICK; + } + if( PB("ng") || zSearch!=0 ){ + tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK); + } + if( PB("nsm") ){ + style_submenu_enable(0); + } + if( PB("brbg") ){ + tmFlags |= TIMELINE_BRCOLOR; + } + if( PB("unhide") ){ + tmFlags |= TIMELINE_UNHIDE; + } + if( PB("ubg") ){ + tmFlags |= TIMELINE_UCOLOR; + } + if( PB("deltabg") ){ + tmFlags |= TIMELINE_DELTA; + } + if( PB("nc") ){ + tmFlags &= ~(TIMELINE_DELTA|TIMELINE_BRCOLOR|TIMELINE_UCOLOR); + tmFlags |= TIMELINE_NOCOLOR; + } + if( showSql ) db_append_dml_to_blob(&allSql); + if( zUses!=0 ){ + int ufid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUses); + if( ufid ){ + if( robot_restrict("timelineX") ) return; + zUses = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ufid); + db_multi_exec("CREATE TEMP TABLE usesfile(rid INTEGER PRIMARY KEY)"); + compute_uses_file("usesfile", ufid, 0); + zType = "ci"; + disableY = 1; + if( !haveParameterN ) nEntry = 0; + }else{ + zUses = 0; + } + } + if( renameOnly ){ + db_multi_exec( + "CREATE TEMP TABLE rnfile(rid INTEGER PRIMARY KEY);" + "INSERT OR IGNORE INTO rnfile" + " SELECT mid FROM mlink WHERE pfnid>0 AND pfnid!=fnid;" + ); + disableY = 1; + } + if( forkOnly ){ + db_multi_exec( + "CREATE TEMP TABLE rnfork(rid INTEGER PRIMARY KEY);\n" + "INSERT OR IGNORE INTO rnfork(rid)\n" + " SELECT pid FROM plink\n" + " WHERE (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n" + " (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n" + " GROUP BY pid\n" + " HAVING count(*)>1;\n" + "INSERT OR IGNORE INTO rnfork(rid)\n" + " SELECT cid FROM plink\n" + " WHERE (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n" + " (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n" + " GROUP BY cid\n" + " HAVING count(*)>1;\n", + TAG_BRANCH, TAG_BRANCH, TAG_BRANCH, TAG_BRANCH + ); + db_multi_exec( + "INSERT OR IGNORE INTO rnfork(rid)\n" + " SELECT cid FROM plink\n" + " WHERE pid IN rnfork\n" + " AND (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n" + " (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n" + " UNION\n" + " SELECT pid FROM plink\n" + " WHERE cid IN rnfork\n" + " AND (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)==\n" + " (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n", + TAG_BRANCH, TAG_BRANCH, TAG_BRANCH, TAG_BRANCH + ); + tmFlags |= TIMELINE_UNHIDE; + zType = "ci"; + disableY = 1; + } + if( bisectLocal && cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){ + int iCurrent = db_lget_int("checkout",0); + char *zPerm = bisect_permalink(); + bisect_create_bilog_table(iCurrent, 0, 1); + tmFlags |= TIMELINE_UNHIDE | TIMELINE_BISECT | TIMELINE_FILLGAPS; + zType = "ci"; + disableY = 1; + style_submenu_element("Permalink", "%R/timeline?bid=%z", zPerm); + }else{ + bisectLocal = 0; + } + if( zBisect!=0 && bisect_create_bilog_table(0, zBisect, 1) ){ + tmFlags |= TIMELINE_UNHIDE | TIMELINE_BISECT | TIMELINE_FILLGAPS; + zType = "ci"; + disableY = 1; + }else{ + zBisect = 0; + } + + style_header("Timeline"); + if( advancedMenu ){ + style_submenu_element("Help", "%R/help/www/timeline"); + } + login_anonymous_available(); + timeline_temp_table(); + blob_zero(&sql); + blob_zero(&desc); + blob_append(&sql, "INSERT OR IGNORE INTO timeline ", -1); + blob_append(&sql, timeline_query_for_www(), -1); + if( PB("fc") || PB("v") || PB("detail") ){ + tmFlags |= TIMELINE_FCHANGES; + } + if( PB("vfx") ){ + tmFlags |= TIMELINE_FORUMTXT; + } + if( (tmFlags & TIMELINE_UNHIDE)==0 ){ + blob_append_sql(&sql, + " AND NOT EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", + TAG_HIDDEN + ); + } + if( from_rid && !to_rid && (P("ft")!=0 || P("bt")!=0) ){ + const char *zTo = P("ft"); + if( zTo ){ + from_to_mode = 1; + to_rid = timeline_endpoint(from_rid, zTo, 1); + }else{ + from_to_mode = 2; + zTo = P("bt"); + to_rid = timeline_endpoint(from_rid, zTo, 0); + } + if( to_rid ){ + cgi_replace_parameter("to", zTo); + if( selectedRid==0 ) selectedRid = from_rid; + if( secondaryRid==0 ) secondaryRid = to_rid; + }else{ + to_rid = from_rid; + blob_appendf(&desc, "There is no path from %h %s to %h.
Instead: ", + P("from"), from_to_mode==1 ? "forward" : "back", zTo); + } + } + if( ((from_rid && to_rid) || (me_rid && you_rid)) && g.perm.Read ){ + /* If from= and to= are present, display all nodes on a path connecting + ** the two */ + PathNode *p = 0; + const char *zFrom = 0; + const char *zTo = 0; + Blob ins; + int nNodeOnPath = 0; + int commonAncs = 0; /* Common ancestors of me_rid and you_rid. */ + int earlierRid = 0, laterRid = 0; + int cost = bShort ? 0 : 1; + int nSkip = 0; + + if( from_rid && to_rid ){ + if( from_to_mode==0 ){ + p = path_shortest(from_rid, to_rid, 0, 0, 0, cost); + }else if( from_to_mode==1 ){ + p = path_shortest(from_rid, to_rid, 0, 2, 0, cost); + earlierRid = commonAncs = from_rid; + laterRid = to_rid; + }else{ + p = path_shortest(to_rid, from_rid, 0, 1, 0, cost); + earlierRid = commonAncs = to_rid; + laterRid = from_rid; + } + zFrom = P("from"); + zTo = zTo2 ? zTo2 : P("to"); + }else{ + commonAncs = path_common_ancestor(me_rid, you_rid); + if( commonAncs!=0 ){ + p = path_first(); + } + if( commonAncs==you_rid ){ + zFrom = P("you"); + zTo = P("me"); + earlierRid = you_rid; + laterRid = me_rid; + }else{ + zFrom = P("me"); + zTo = P("you"); + earlierRid = me_rid; + laterRid = you_rid; + } + } + blob_init(&ins, 0, 0); + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS pathnode(x INTEGER PRIMARY KEY);" + ); + if( p ){ + int cnt = 4; + blob_init(&ins, 0, 0); + blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid); + if( p->u.pTo==0 ) bMin = 0; + for(p=p->u.pTo; p; p=p->u.pTo){ + if( bMin + && p->u.pTo!=0 + && fossil_strcmp(path_branch(p->pFrom),path_branch(p))==0 + && fossil_strcmp(path_branch(p),path_branch(p->u.pTo))==0 + ){ + nSkip++; + }else if( cnt==8 ){ + blob_append_sql(&ins, ",\n (%d)", p->rid); + cnt = 0; + }else{ + cnt++; + blob_append_sql(&ins, ",(%d)", p->rid); + } + } + style_submenu_checkbox("min","Brief",0,0); + } + path_reset(); + db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/); + blob_reset(&ins); + if( related ){ + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS related(x INTEGER PRIMARY KEY);" + "INSERT OR IGNORE INTO related(x)" + " SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;" + ); + if( related==1 ){ + db_multi_exec( + "INSERT OR IGNORE INTO related(x)" + " SELECT cid FROM plink WHERE pid IN pathnode;" + ); + } + if( showCherrypicks ){ + db_multi_exec( + "INSERT OR IGNORE INTO related(x)" + " SELECT parentid FROM cherrypick WHERE childid IN pathnode;" + ); + if( related==1 ){ + db_multi_exec( + "INSERT OR IGNORE INTO related(x)" + " SELECT childid FROM cherrypick WHERE parentid IN pathnode;" + ); + } + } + if( earlierRid && laterRid && commonAncs==earlierRid ){ + /* On a query with me=XXX, you=YYY, and rel, omit all nodes that + ** are not ancestors of either XXX or YYY, as those nodes tend to + ** be extraneous */ + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)" + ); + compute_ancestors(laterRid, 0, 0, earlierRid); + db_multi_exec( + "DELETE FROM related WHERE x NOT IN ok;" + ); + } + db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related"); + } + add_extra_rids("pathnode",P("x")); + add_extra_rids("pathnode",P("sel1")); + add_extra_rids("pathnode",P("sel2")); + blob_append_sql(&sql, " AND event.objid IN pathnode"); + if( zChng ){ + db_multi_exec( + "DELETE FROM pathnode\n" + " WHERE NOT EXISTS(SELECT 1 FROM mlink, filename\n" + " WHERE mlink.mid=x\n" + " AND mlink.fnid=filename.fnid\n" + " AND %s)", + glob_expr("filename.name", zChng) + ); + } + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; + db_multi_exec("%s", blob_sql_text(&sql)); + if( advancedMenu ){ + style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0); + } + nNodeOnPath = db_int(0, "SELECT count(*) FROM temp.pathnode"); + if( nNodeOnPath==1 && from_to_mode>0 ){ + blob_appendf(&desc,"Check-in "); + }else if( bMin ){ + blob_appendf(&desc, "%d of %d check-ins along the path from ", + nNodeOnPath, nNodeOnPath+nSkip); + }else{ + blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath); + } + if( from_rid==selectedRid ){ + blob_appendf(&desc, ""); + } + blob_appendf(&desc, "%z%h", href("%R/info/%h", zFrom), zFrom); + if( from_rid==selectedRid ) blob_appendf(&desc, ""); + if( nNodeOnPath==1 && from_to_mode>0 ){ + blob_appendf(&desc, " only"); + }else{ + blob_append(&desc, " to ", -1); + if( to_rid==secondaryRid ){ + blob_appendf(&desc,""); + } + blob_appendf(&desc, "%z%h", href("%R/info/%h",zTo), zTo); + if( to_rid==secondaryRid ) blob_appendf(&desc, ""); + if( related ){ + int nRelated = db_int(0, "SELECT count(*) FROM timeline") - nNodeOnPath; + if( nRelated>0 ){ + blob_appendf(&desc, " and %d related check-in%s", nRelated, + nRelated>1 ? "s" : ""); + } + } + } + addFileGlobDescription(zChng, &desc); + }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){ + /* If either p= or d= or both are present, ignore all other parameters + ** other than n=, ft=, and bt= */ + const char *zBaseName = 0; + int np = 0, nd; + const char *zBackTo = 0; + const char *zFwdTo = 0; + int ridBackTo = 0; + int ridFwdTo = 0; + int bBackAdded = 0; /* True if the zBackTo node was added */ + int bFwdAdded = 0; /* True if the zBackTo node was added */ + int bSeparateDandP = 0; /* p_rid & d_rid both exist and are distinct */ + + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; + if( p_rid && d_rid && p_rid!=d_rid ){ + bSeparateDandP = 1; + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS ok_d(rid INTEGER PRIMARY KEY)" + ); + }else{ + zBaseName = p_rid ? zDPNameP : zDPNameD; + } + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)" + ); + add_extra_rids("ok", P("x")); + add_extra_rids("ok", P("sel1")); + add_extra_rids("ok", P("sel2")); + blob_append_sql(&sql, " AND event.objid IN ok"); + nd = 0; + if( d_rid ){ + double rStopTime = 9e99; + zFwdTo = P("ft"); + if( zFwdTo && bSeparateDandP ){ + if( zError==0 ){ + zError = "Cannot use the ft= query parameter when both p= and d= " + "are used and have distinct values."; + } + zFwdTo = 0; + } + if( zFwdTo ){ + double rStartDate = mtime_of_rid(d_rid, 0.0); + ridFwdTo = first_checkin_with_tag_after_date(zFwdTo, rStartDate); + if( ridFwdTo==0 ){ + ridFwdTo = name_to_typed_rid(zBackTo,"ci"); + } + if( ridFwdTo ){ + if( !haveParameterN ) nEntry = 0; + rStopTime = mtime_of_rid(ridFwdTo, 9e99); + } + }else if( bSeparateDandP ){ + rStopTime = mtime_of_rid(p_rid, 9e99); + nEntry = 0; + } + if( rStopTime<9e99 ){ + rStopTime += 5.8e-6; /* Round up by 1/2 second */ + } + db_multi_exec( + "WITH RECURSIVE dx(rid,mtime) AS (\n" + " SELECT %d, 0\n" + " UNION\n" + " SELECT plink.cid, plink.mtime FROM dx, plink\n" + " WHERE plink.pid=dx.rid\n" + " AND plink.mtime<=%.*g\n" + " ORDER BY 2\n" + ")\n" + "INSERT OR IGNORE INTO ok SELECT rid FROM dx LIMIT %d", + d_rid, rStopTime<8e99 ? 17 : 2, rStopTime, nEntry<=0 ? -1 : nEntry+1 + ); + if( ridFwdTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridFwdTo) ){ + db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridFwdTo); + bFwdAdded = 1; + } + if( bSeparateDandP ){ + db_multi_exec( + "INSERT INTO ok_d SELECT rid FROM ok;" + "DELETE FROM ok;" + ); + }else{ + removeFileGlobFromOk(zChng); + nd = db_int(0, "SELECT count(*)-1 FROM ok"); + if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql)); + if( nd>0 || p_rid==0 ){ + blob_appendf(&desc, "%d descendant%s", + nd>=0 ? nd : 0,(1==nd)?"":"s"); + } + if( useDividers && !selectedRid ) selectedRid = d_rid; + db_multi_exec("DELETE FROM ok"); + } + } + if( p_rid ){ + zBackTo = P("bt"); + if( zBackTo && bSeparateDandP ){ + if( zError==0 ){ + zError = "Cannot use the bt= query parameter when both p= and d= " + "are used and have distinct values."; + } + zBackTo = 0; + } + if( zBackTo ){ + double rDateLimit = mtime_of_rid(p_rid, 0.0); + ridBackTo = last_checkin_with_tag_before_date(zBackTo, rDateLimit); + if( ridBackTo==0 ){ + ridBackTo = name_to_typed_rid(zBackTo,"ci"); + } + if( ridBackTo && !haveParameterN ) nEntry = 0; + }else if( bSeparateDandP ){ + ridBackTo = d_rid; + nEntry = 0; + } + compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0, ridBackTo); + if( ridBackTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridBackTo) ){ + db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridBackTo); + bBackAdded = 1; + } + if( bSeparateDandP ){ + db_multi_exec("DELETE FROM ok WHERE rid NOT IN ok_d;"); + removeFileGlobFromOk(zChng); + db_multi_exec("%s", blob_sql_text(&sql)); + }else{ + removeFileGlobFromOk(zChng); + np = db_int(0, "SELECT count(*)-1 FROM ok"); + if( np>0 || nd==0 ){ + if( nd>0 ) blob_appendf(&desc, " and "); + blob_appendf(&desc, "%d ancestor%s", + np>=0 ? np : 0, (1==np)?"":"s"); + db_multi_exec("%s", blob_sql_text(&sql)); + } + if( useDividers && !selectedRid ) selectedRid = p_rid; + } + } + + if( bSeparateDandP ){ + int n = db_int(0, "SELECT count(*) FROM ok"); + blob_reset(&desc); + blob_appendf(&desc, + "%d check-ins that are derived from %z%h" + " and contribute to %z%h", + n, + href("%R/info?name=%h",zDPNameD),zDPNameD, + href("%R/info?name=%h",zDPNameP),zDPNameP + ); + ridBackTo = 0; + ridFwdTo = 0; + }else{ + blob_appendf(&desc, " of %z%h", + href("%R/info?name=%h", zBaseName), zBaseName); + } + if( ridBackTo ){ + if( np==0 ){ + blob_reset(&desc); + blob_appendf(&desc, + "Check-in %z%h only (%z%h does not precede it)", + href("%R/info?name=%h",zBaseName), zBaseName, + href("%R/info?name=%h",zBackTo), zBackTo); + }else{ + blob_appendf(&desc, " back to %z%h%s", + href("%R/info?name=%h",zBackTo), zBackTo, + bBackAdded ? " (not a direct ancestor)" : ""); + if( ridFwdTo && zFwdTo ){ + blob_appendf(&desc, " and up to %z%h%s", + href("%R/info?name=%h",zFwdTo), zFwdTo, + bFwdAdded ? " (not a direct descendant)" : ""); + } + } + }else if( ridFwdTo ){ + if( nd==0 ){ + blob_reset(&desc); + blob_appendf(&desc, + "Check-in %z%h only (%z%h does not follow it)", + href("%R/info?name=%h",zBaseName), zBaseName, + href("%R/info?name=%h",zFwdTo), zFwdTo); + }else{ + blob_appendf(&desc, " up to %z%h%s", + href("%R/info?name=%h",zFwdTo), zFwdTo, + bFwdAdded ? " (not a direct descendant)":""); + } + } + if( zChng ){ + if( strstr(blob_str(&desc)," that ") ) blob_appendf(&desc, " and"); + blob_appendf(&desc, " that make changes to files matching \"%h\"", zChng); + } + if( advancedMenu ){ + style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0); + } + style_submenu_entry("n","Max:",4,0); + timeline_y_submenu(1); + }else if( f_rid && g.perm.Read ){ + /* If f= is present, ignore all other parameters other than n= */ + char *zUuid; + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);" + "INSERT INTO ok VALUES(%d);" + "INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;" + "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;", + f_rid, f_rid, f_rid + ); + if( showCherrypicks ){ + db_multi_exec( + "INSERT OR IGNORE INTO ok SELECT parentid FROM cherrypick" + " WHERE childid=%d;" + "INSERT OR IGNORE INTO ok SELECT childid FROM cherrypick" + " WHERE parentid=%d;", + f_rid, f_rid + ); + } + blob_append_sql(&sql, " AND event.objid IN ok"); + db_multi_exec("%s", blob_sql_text(&sql)); + if( useDividers && !selectedRid ) selectedRid = f_rid; + blob_appendf(&desc, "Parents and children of check-in "); + zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid); + blob_appendf(&desc, "%z[%S]", href("%R/info/%!S", zUuid), zUuid); + tmFlags |= TIMELINE_XMERGE; + if( advancedMenu ){ + style_submenu_checkbox("unhide", "Unhide", 0, 0); + style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0); + } + }else{ + /* Otherwise, a timeline based on a span of time */ + int n; + const char *zEType = "event"; + char *zDate; + Blob cond; + blob_zero(&cond); + tmFlags |= TIMELINE_FILLGAPS; + if( zChng && *zChng ){ + addFileGlobExclusion(zChng, &cond); + tmFlags |= TIMELINE_XMERGE; + } + if( zUses ){ + blob_append_sql(&cond, " AND event.objid IN usesfile\n"); + } + if( renameOnly ){ + blob_append_sql(&cond, " AND event.objid IN rnfile\n"); + } + if( forkOnly ){ + blob_append_sql(&cond, " AND event.objid IN rnfork\n"); + } + if( cpOnly && showCherrypicks ){ + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS cpnodes(rid INTEGER PRIMARY KEY);" + "INSERT OR IGNORE INTO cpnodes SELECT childid FROM cherrypick;" + "INSERT OR IGNORE INTO cpnodes SELECT parentid FROM cherrypick;" + ); + blob_append_sql(&cond, " AND event.objid IN cpnodes\n"); + } + if( bisectLocal || zBisect!=0 ){ + blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog)\n"); + } + if( zYearMonth ){ + char *zNext; + int bZulu = 0; + const char *zTZMod; + zYearMonth = timeline_expand_datetime(zYearMonth, &bZulu); + zYearMonth = mprintf("%.7s", zYearMonth); + if( db_int(0,"SELECT julianday('%q-01') IS NULL", zYearMonth) ){ + zYearMonth = db_text(0, "SELECT strftime('%%Y-%%m','now');"); + } + zTZMod = (bZulu==0 && fossil_ui_localtime()) ? "utc" : "+00:00"; + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid" + " AND mtime>=julianday('%q-01',%Q)%s)", + zYearMonth, zTZMod, blob_sql_text(&cond)) + ){ + zNext = db_text(0, "SELECT strftime('%%Y%%m%q','%q-01','+1 month');", + &"Z"[!bZulu], zYearMonth); + zNewerButton = fossil_strdup(url_render(&url, "ym", zNext, 0, 0)); + zNewerButtonLabel = "Following month"; + fossil_free(zNext); + } + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid" + " AND mtime=julianday('%q-01',%Q)" + " AND event.mtime=julianday(%Q,%Q)%s)", + zYearWeekStart, zTZMod, blob_sql_text(&cond)) + ){ + zNext = db_text(0, "SELECT strftime('%%Y%%W%q',%Q,'+7 day');", + &"Z"[!bZulu], zYearWeekStart); + zNewerButton = fossil_strdup(url_render(&url, "yw", zNext, 0, 0)); + zNewerButtonLabel = "Following week"; + fossil_free(zNext); + } + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid" + " AND mtime=julianday(%Q,%Q)" + " AND event.mtime0 && db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid" + " AND mtime>=julianday(%Q,'1 day',%Q)%s)", + zEnd, zTZMod, blob_sql_text(&cond)) + ){ + zNext = db_text(0, + "SELECT strftime('%%Y%%m%%d-',%Q,'%d days')||" + "strftime('%%Y%%m%%d%q',%Q,'%d day');", + zStart, nDay, &"Z"[!bZulu], zEnd, nDay); + zNewerButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0)); + zNewerButtonLabel = mprintf("Following %d days", nDay); + fossil_free(zNext); + } + if( nDay>1 && db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid" + " AND mtime=julianday(%Q,%Q)" + " AND event.mtime=julianday(%Q,'+1 day',%Q)%s)", + zDay, zTZMod, blob_sql_text(&cond)) + ){ + zNext = db_text(0,"SELECT strftime('%%Y%%m%%d%q',%Q,'+1 day');", + &"Z"[!bZulu], zDay); + zNewerButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0)); + zNewerButtonLabel = "Following day"; + fossil_free(zNext); + } + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid" + " AND mtime=julianday(%Q,%Q)" + " AND event.mtime=julianday('now','-%d days') ", + nDays); + nEntry = -1; + } + else if( zYear && + ((4==strlen(zYear) && atoi(zYear)>1900) + || (1==strlen(zYear) && 0==atoi(zYear)))){ + int year = atoi(zYear); + char *zNext = 0; + if(0==year){/*use current year*/ + Stmt qy; + db_prepare(&qy, "SELECT strftime('%%Y','now')"); + db_step(&qy); + year = db_column_int(&qy, 0); + zYear = fossil_strdup(db_column_text(&qy, 0)); + db_finalize(&qy); + }else{ + zNext = mprintf("%d", year+1); + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid AND strftime('%%Y',mtime)=%Q %s)", + zNext, blob_sql_text(&cond)) + ){ + zNewerButton = fossil_strdup(url_render(&url, "year", zNext, 0, 0)); + zNewerButtonLabel = "Following year"; + } + fossil_free(zNext); + } + zNext = mprintf("%d", year-1); + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid AND strftime('%%Y',mtime)=%Q %s)", + zNext, blob_sql_text(&cond)) + ){ + zOlderButton = fossil_strdup(url_render(&url, "year", zNext, 0, 0)); + zOlderButtonLabel = "Previous year"; + } + fossil_free(zNext); + blob_append_sql(&cond, " AND %Q=strftime('%%Y',event.mtime) ", + zYear); + nEntry = -1; + } + if( zTagSql ){ + db_multi_exec( + "CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);" + "INSERT OR IGNORE INTO selected_nodes\n" + " SELECT tagxref.rid FROM tagxref NATURAL JOIN tag\n" + " WHERE tagtype>0\n" + " AND %s", zTagSql/*safe-for-%s*/ + ); + if( zMark ){ + /* If the t=release option is used with m=UUID, then also + ** include the UUID check-in in the display list */ + int ridMark = name_to_rid(zMark); + db_multi_exec( + "INSERT OR IGNORE INTO selected_nodes(rid) VALUES(%d)", ridMark); + } + add_extra_rids("selected_nodes",P("x")); + add_extra_rids("selected_nodes",P("sel1")); + add_extra_rids("selected_nodes",P("sel2")); + if( related==0 ){ + blob_append_sql(&cond, " AND blob.rid IN selected_nodes"); + }else{ + db_multi_exec( + "CREATE TEMP TABLE related_nodes(rid INTEGER PRIMARY KEY);" + "INSERT INTO related_nodes SELECT rid FROM selected_nodes;" + ); + blob_append_sql(&cond, " AND blob.rid IN related_nodes"); + /* The next two blob_appendf() calls add SQL that causes check-ins that + ** are not part of the branch which are parents or children of the + ** branch to be included in the report. These related check-ins are + ** useful in helping to visualize what has happened on a quiescent + ** branch that is infrequently merged with a much more activate branch. + */ + db_multi_exec( + "INSERT OR IGNORE INTO related_nodes\n" + " SELECT pid FROM selected_nodes CROSS JOIN plink\n" + " WHERE selected_nodes.rid=plink.cid;" + ); + if( related==1 ){ + db_multi_exec( + "INSERT OR IGNORE INTO related_nodes\n" + " SELECT cid FROM selected_nodes CROSS JOIN plink\n" + " WHERE selected_nodes.rid=plink.pid;" + ); + if( showCherrypicks ){ + db_multi_exec( + "INSERT OR IGNORE INTO related_nodes\n" + " SELECT childid FROM selected_nodes CROSS JOIN cherrypick\n" + " WHERE selected_nodes.rid=cherrypick.parentid;" + ); + } + } + if( showCherrypicks ){ + db_multi_exec( + "INSERT OR IGNORE INTO related_nodes\n" + " SELECT parentid FROM selected_nodes CROSS JOIN cherrypick\n" + " WHERE selected_nodes.rid=cherrypick.childid;" + ); + } + if( (tmFlags & TIMELINE_UNHIDE)==0 ){ + db_multi_exec( + "DELETE FROM related_nodes\n" + " WHERE rid IN (SELECT related_nodes.rid\n" + " FROM related_nodes, tagxref\n" + " WHERE tagid=%d AND tagtype>0\n" + " AND tagxref.rid=related_nodes.rid)", + TAG_HIDDEN + ); + } + } + } + if( (zType[0]=='w' && !g.perm.RdWiki) + || (zType[0]=='t' && !g.perm.RdTkt) + || (zType[0]=='n' && !g.perm.RdTkt) + || (zType[0]=='e' && !g.perm.RdWiki) + || (zType[0]=='c' && !g.perm.Read) + || (zType[0]=='g' && !g.perm.Read) + || (zType[0]=='f' && !g.perm.RdForum) + || (zType[0]=='h' && !g.perm.RdForum) + ){ + zType = "all"; + } + if( zType[0]=='a' ){ + if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){ + char cSep = '('; + blob_append_sql(&cond, " AND event.type IN "); + if( g.perm.Read ){ + blob_append_sql(&cond, "%c'ci','g'", cSep); + cSep = ','; + } + if( g.perm.RdWiki ){ + blob_append_sql(&cond, "%c'w','e'", cSep); + cSep = ','; + } + if( g.perm.RdTkt ){ + blob_append_sql(&cond, "%c't'", cSep); + cSep = ','; + } + if( g.perm.RdForum ){ + blob_append_sql(&cond, "%c'f'", cSep); + cSep = ','; + } + blob_append_sql(&cond, ")"); + } + }else{ /* zType!="all" */ + if( zType[0]=='n' ){ + blob_append_sql(&cond, + " AND event.type='t' AND event.comment GLOB 'New ticket*'"); + }else if( zType[0]=='h' ){ + blob_append_sql(&cond, + " AND event.type='f' AND event.comment GLOB 'Post:*'"); + }else{ + blob_append_sql(&cond, " AND event.type=%Q", zType); + } + if( zType[0]=='c' ){ + zEType = "check-in"; + }else if( zType[0]=='w' ){ + zEType = "wiki"; + }else if( zType[0]=='t' ){ + zEType = "ticket change"; + }else if( zType[0]=='n' ){ + zEType = "new ticket"; + }else if( zType[0]=='e' ){ + zEType = "technical note"; + }else if( zType[0]=='g' ){ + zEType = "tag"; + }else if( zType[0]=='f' ){ + zEType = "forum post"; + }else if( zType[0]=='h' ){ + zEType = "new forum thread"; + } + } + if( zUser ){ + int n = db_int(0,"SELECT count(*) FROM event" + " WHERE user=%Q OR euser=%Q", zUser, zUser); + if( n<=nEntry ){ + nEntry = -1; + } + blob_append_sql(&cond, " AND (event.user=%Q OR event.euser=%Q)", + zUser, zUser); + zThisUser = zUser; + } + if( zSearch ){ + if( tmFlags & TIMELINE_FORUMTXT ){ + sqlite3_create_function(g.db, "forum_post_content", 1, SQLITE_UTF8, + 0, forum_post_content_function, 0, 0); + blob_append_sql(&cond, + " AND (event.comment LIKE '%%%q%%'" + " OR event.brief LIKE '%%%q%%'" + " OR (event.type=='f' AND" + " forum_post_content(event.objid) LIKE '%%%q%%'))", + zSearch, zSearch, zSearch); + }else{ + blob_append_sql(&cond, + " AND (event.comment LIKE '%%%q%%' OR event.brief LIKE '%%%q%%')", + zSearch, zSearch); + } + } + rBefore = symbolic_name_to_mtime(zBefore, &zBefore, 1); + rAfter = symbolic_name_to_mtime(zAfter, &zAfter, 0); + rCirca = symbolic_name_to_mtime(zCirca, &zCirca, 0); + blob_append_sql(&sql, "%s", blob_sql_text(&cond)); + if( rAfter>0.0 ){ + if( rBefore>0.0 ){ + blob_append_sql(&sql, + " AND event.mtime>=%.17g AND event.mtime<=%.17g\n" + " ORDER BY event.mtime ASC", rAfter-ONE_SECOND, rBefore+ONE_SECOND); + nEntry = -1; + }else{ + blob_append_sql(&sql, + " AND event.mtime>=%.17g\n ORDER BY event.mtime ASC", + rAfter-ONE_SECOND); + } + zCirca = 0; + url_add_parameter(&url, "c", 0); + }else if( rBefore>0.0 ){ + blob_append_sql(&sql, + " AND event.mtime<=%.17g\n ORDER BY event.mtime DESC", + rBefore+ONE_SECOND); + zCirca = 0; + url_add_parameter(&url, "c", 0); + }else if( rCirca>0.0 ){ + Blob sql2; + blob_init(&sql2, blob_sql_text(&sql), -1); + blob_append_sql(&sql2, + " AND event.mtime>=%f\n ORDER BY event.mtime ASC", rCirca); + if( nEntry>0 ){ + blob_append_sql(&sql2," LIMIT %d", (nEntry+1)/2); + } + db_multi_exec("%s", blob_sql_text(&sql2)); + if( nEntry>0 ){ + nEntry -= db_int(0,"select count(*) from timeline"); + if( nEntry<=0 ) nEntry = 1; + } + blob_reset(&sql2); + blob_append_sql(&sql, + " AND event.mtime<=%f\n ORDER BY event.mtime DESC", + rCirca + ); + if( zMark==0 ) zMark = zCirca; + }else{ + blob_append_sql(&sql, " ORDER BY event.mtime DESC"); + } + if( nEntry>0 ) blob_append_sql(&sql, " LIMIT %d", nEntry); + db_multi_exec("%s", blob_sql_text(&sql)); + + n = db_int(0, "SELECT count(*) FROM timeline WHERE etype!='div' /*scan*/"); + zPlural = n==1 ? "" : "s"; + if( zYearMonth ){ + blob_appendf(&desc, "%d %s%s for the month beginning %h", + n, zEType, zPlural, zYearMonth); + }else if( zYearWeek ){ + char *zEnd = db_text("?","SELECT date(%Q,'weekday 0')",zYearWeekStart); + blob_appendf(&desc, "%d %s%s for week %h spanning %h to %h", + n, zEType, zPlural, zYearWeek, zYearWeekStart, zEnd); + fossil_free(zEnd); + }else if( zDay ){ + blob_appendf(&desc, "%d %s%s occurring on %h", n, zEType, zPlural, zDay); + }else if( zNDays ){ + blob_appendf(&desc, "%d %s%s within the past %d day%s", + n, zEType, zPlural, nDays, nDays>1 ? "s" : ""); + }else if( zBefore==0 && zCirca==0 && n>=nEntry && nEntry>0 ){ + blob_appendf(&desc, "%d most recent %s%s", n, zEType, zPlural); + }else{ + blob_appendf(&desc, "%d %s%s", n, zEType, zPlural); + } + if( zUses ){ + char *zFilenames = names_of_file(zUses); + blob_appendf(&desc, " using file %s version %z%S", zFilenames, + href("%R/artifact/%!S",zUses), zUses); + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; + } + if( renameOnly ){ + blob_appendf(&desc, " that contain filename changes"); + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; + } + if( forkOnly ){ + blob_appendf(&desc, " associated with forks"); + tmFlags |= TIMELINE_DISJOINT; + } + if( bisectLocal || zBisect!=0 ){ + blob_appendf(&desc, " in a bisect"); + tmFlags |= TIMELINE_DISJOINT; + } + if( cpOnly && showCherrypicks ){ + blob_appendf(&desc, " that participate in a cherrypick merge"); + tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT; + } + if( zUser ){ + if( g.perm.Admin ){ + blob_appendf(&desc, " by user %h", + zUser, zUser); + }else{ + blob_appendf(&desc, " by user %h", zUser); + } + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; + } + if( zTagSql ){ + if( matchStyle==MS_EXACT || matchStyle==MS_BRLIST ){ + if( related ){ + blob_appendf(&desc, " related to %h", zMatchDesc); + }else{ + blob_appendf(&desc, " tagged with %h", zMatchDesc); + } + }else{ + if( related ){ + blob_appendf(&desc, " related to tags matching %h", zMatchDesc); + }else{ + blob_appendf(&desc, " with tags matching %h", zMatchDesc); + } + } + if( zMark ){ + blob_appendf(&desc," plus check-in \"%h\"", zMark); + } + tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS; + } + addFileGlobDescription(zChng, &desc); + if( rAfter>0.0 ){ + if( rBefore>0.0 ){ + blob_appendf(&desc, " occurring between %h and %h.
", + zAfter, zBefore); + }else{ + blob_appendf(&desc, " occurring on or after %h.
", zAfter); + } + }else if( rBefore>0.0 ){ + blob_appendf(&desc, " occurring on or before %h.
", zBefore); + }else if( rCirca>0.0 ){ + blob_appendf(&desc, " occurring around %h.
", zCirca); + } + if( zSearch ){ + blob_appendf(&desc, " matching \"%h\"", zSearch); + } + if( g.perm.Hyperlink ){ + static const char *const azMatchStyles[] = { + "exact", "Exact", "glob", "Glob", "like", "Like", "regexp", "Regexp", + "brlist", "List" + }; + double rDate; + zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/"); + if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){ + zDate = fossil_strdup((zAfter ? zAfter : zBefore)); + } + if( zDate ){ + rDate = symbolic_name_to_mtime(zDate, 0, 0); + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid AND mtime<=%.17g%s)", + rDate-ONE_SECOND, blob_sql_text(&cond)) + ){ + zOlderButton = fossil_strdup(url_render(&url, "b", zDate, "a", 0)); + zOlderButtonLabel = "More"; + } + free(zDate); + } + zDate = db_text(0, "SELECT max(timestamp) FROM timeline /*scan*/"); + if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){ + zDate = fossil_strdup((zBefore ? zBefore : zAfter)); + } + if( zDate ){ + rDate = symbolic_name_to_mtime(zDate, 0, 0); + if( db_int(0, + "SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob" + " WHERE blob.rid=event.objid AND mtime>=%.17g%s)", + rDate+ONE_SECOND, blob_sql_text(&cond)) + ){ + zNewerButton = fossil_strdup(url_render(&url, "a", zDate, "b", 0)); + zNewerButtonLabel = "More"; + } + free(zDate); + } + if( advancedMenu ){ + if( zType[0]=='a' || zType[0]=='c' ){ + style_submenu_checkbox("unhide", "Unhide", 0, 0); + } + style_submenu_checkbox("v", "Files",(zType[0]!='a' && zType[0]!='c'),0); + } + style_submenu_entry("n","Max:",4,0); + timeline_y_submenu(disableY); + if( advancedMenu ){ + style_submenu_entry("t", "Tag Filter:", -8, 0); + style_submenu_multichoice("ms", count(azMatchStyles)/2,azMatchStyles,0); + } + } + blob_zero(&cond); + } + if( showSql ){ + db_append_dml_to_blob(0); + @
%h(blob_str(&allSql))
+ blob_reset(&allSql); + } + if( search_restrict(SRCH_CKIN)!=0 ){ + style_submenu_element("Search", "%R/search?y=c"); + } + if( advancedMenu ){ + style_submenu_element("Basic", "%s", + url_render(&url, "advm", "0", "udc", "1")); + }else{ + style_submenu_element("Advanced", "%s", + url_render(&url, "advm", "1", "udc", "1")); + } + if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID; + if( useDividers && zMark && zMark[0] ){ + double r = symbolic_name_to_mtime(zMark, 0, 0); + if( r>0.0 && !selectedRid ) selectedRid = timeline_add_divider(r); + } + blob_zero(&sql); + if( PB("oldestfirst") ){ + db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby ASC /*scan*/"); + tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK); + }else{ + db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/"); + } + if( fossil_islower(desc.aData[0]) ){ + desc.aData[0] = fossil_toupper(desc.aData[0]); + } + if( zBrName ){ + if( !PB("nowiki") + && wiki_render_associated("branch", zBrName, WIKIASSOC_ALL) + ){ + @
%b(&desc)
+ } else{ + @

%b(&desc)

+ } + style_submenu_element("Diff", "%R/vdiff?branch=%T", zBrName); + }else + if( zTagName + && matchStyle==MS_EXACT + && zBrName==0 + && !PB("nowiki") + && wiki_render_associated("tag", zTagName, WIKIASSOC_ALL) + ){ + @
%b(&desc)
+ } else{ + @

%b(&desc)

+ } + blob_reset(&desc); + + /* Report any errors. */ + if( zError ){ + @

%h(zError)

+ } + + /* Swap zNewer and zOlder buttons if we display oldestfirst */ + if( PB("oldestfirst") ){ + char *zSwap = zNewerButton; + char *zSwapLabel = zNewerButtonLabel; + zNewerButton = zOlderButton; + zNewerButtonLabel = zOlderButtonLabel; + zOlderButton = zSwap; + zOlderButtonLabel = zSwapLabel; + } + + if( zNewerButton ){ + @ %z(chref("button","%s",zNewerButton))%h(zNewerButtonLabel)\ + @  ↑ + } + cgi_check_for_malice(); + { + TimelineXtra xtra; + const char *zPattern = P("sl"); + memset(&xtra, 0, sizeof(xtra)); + if( zPattern!=0 ){ + MatchStyle ms; + if( zMatchStyle!=0 ){ + ms = matchStyle; + }else{ + ms = strpbrk(zPattern,"*[?")!=0 ? MS_GLOB : MS_BRLIST; + } + xtra.pLeftBranch = match_create(ms,zPattern); + }else{ + xtra.pLeftBranch = match_create(matchStyle, zBrName?zBrName:zTagName); + } + xtra.zThisUser = zThisUser; + xtra.zThisTag = zThisTag; + xtra.selectedRid = selectedRid; + xtra.secondRid = secondaryRid; + www_print_timeline(&q, tmFlags, &xtra); + match_free(xtra.pLeftBranch); + } + db_finalize(&q); + if( zOlderButton ){ + @ %z(chref("button","%s",zOlderButton))%h(zOlderButtonLabel)\ + @  ↓ + } + document_emit_js(/*handles pikchrs rendered above*/); + blob_reset(&sql); + blob_reset(&desc); + style_finish_page(); +} + +/* +** Translate a timeline entry into the printable format by +** converting every %-substitutions as follows: +** +** %n newline +** %% a raw % +** %H commit hash +** %h abbreviated commit hash +** %a author name +** %d date +** %c comment (\n, \t replaced by space, \r deleted) +** %b branch +** %t tags +** %p phase (zero or more of: *CURRENT*, *MERGE*, *FORK*, +** *UNPUBLISHED*, *LEAF*, *BRANCH*) +** +** The returned string is obtained from fossil_malloc() and should +** be freed by the caller. +*/ +static char *timeline_entry_subst( + const char *zFormat, + int *nLine, + const char *zId, + const char *zDate, + const char *zUser, + const char *zCom, + const char *zBranch, + const char *zTags, + const char *zPhase +){ + Blob r, co; + int i, j; + blob_init(&r, 0, 0); + blob_init(&co, 0, 0); + + if( 0==zCom ){ + zCom = "(NULL)"; + } + + /* Replace LF and tab with space, delete CR */ + while( zCom[0] ){ + for(j=0; zCom[j] && zCom[j]!='\r' && zCom[j]!='\n' && zCom[j]!='\t'; j++){} + blob_append(&co, zCom, j); + if( zCom[j]==0 ) break; + if( zCom[j]!='\r') + blob_append(&co, " ", 1); + zCom += j+1; + } + blob_str(&co); + + *nLine = 1; + while( zFormat[0] ){ + for(i=0; zFormat[i] && zFormat[i]!='%'; i++){} + blob_append(&r, zFormat, i); + if( zFormat[i]==0 ) break; + if( zFormat[i+1]=='%' ){ + blob_append(&r, "%", 1); + zFormat += i+2; + }else if( zFormat[i+1]=='n' ){ + blob_append(&r, "\n", 1); + *nLine += 1; + zFormat += i+2; + }else if( zFormat[i+1]=='H' ){ + blob_append(&r, zId, -1); + zFormat += i+2; + }else if( zFormat[i+1]=='h' ){ + char *zFree = 0; + zFree = mprintf("%S", zId); + blob_append(&r, zFree, -1); + fossil_free(zFree); + zFormat += i+2; + }else if( zFormat[i+1]=='d' ){ + blob_append(&r, zDate, -1); + zFormat += i+2; + }else if( zFormat[i+1]=='a' ){ + blob_append(&r, zUser, -1); + zFormat += i+2; + }else if( zFormat[i+1]=='c' ){ + blob_append(&r, co.aData, -1); + zFormat += i+2; + }else if( zFormat[i+1]=='b' ){ + if( zBranch ) blob_append(&r, zBranch, -1); + zFormat += i+2; + }else if( zFormat[i+1]=='t' ){ + blob_append(&r, zTags, -1); + zFormat += i+2; + }else if( zFormat[i+1]=='p' ){ + blob_append(&r, zPhase, -1); + zFormat += i+2; + }else{ + blob_append(&r, zFormat+i, 1); + zFormat += i+1; + } + } + fossil_free(co.aData); + blob_str(&r); + return r.aData; +} + +/* +** The input query q selects various records. Print a human-readable +** summary of those records. +** +** Limit number of lines or entries printed to nLimit. If nLimit is zero +** there is no limit. If nLimit is greater than zero, limit the number of +** complete entries printed. If nLimit is less than zero, attempt to limit +** the number of lines printed (this is basically the legacy behavior). +** The line limit, if used, is approximate because it is only checked on a +** per-entry basis. If verbose mode, the file name details are considered +** to be part of the entry. +** +** The query should return these columns: +** +** 0. rid +** 1. uuid +** 2. Date/Time +** 3. Comment string, user, and tags +** 4. Number of non-merge children +** 5. Number of parents +** 6. mtime +** 7. branch +** 8. event-type: 'ci', 'w', 't', 'f', and so forth. +** 9. comment +** 10. user +** 11. tags +*/ +void print_timeline(Stmt *q, int nLimit, int width, const char *zFormat, + int verboseFlag){ + int nAbsLimit = (nLimit >= 0) ? nLimit : -nLimit; + int nLine = 0; + int nEntry = 0; + char zPrevDate[20]; + const char *zCurrentUuid = 0; + int fchngQueryInit = 0; /* True if fchngQuery is initialized */ + Stmt fchngQuery; /* Query for file changes on check-ins */ + int rc; + /* True: separate entries with a newline after file listing */ + int bVerboseNL = (zFormat && + (fossil_strcmp(zFormat, TIMELINE_FMT_ONELINE)!=0)); + /* True: separate entries with a newline even with no file listing */ + int bNoVerboseNL = (zFormat && + (fossil_strcmp(zFormat, TIMELINE_FMT_MEDIUM)==0 || + fossil_strcmp(zFormat, TIMELINE_FMT_FULL)==0)); + + zPrevDate[0] = 0; + if( g.localOpen ){ + int rid = db_lget_int("checkout", 0); + zCurrentUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); + } + + while( (rc=db_step(q))==SQLITE_ROW ){ + int rid = db_column_int(q, 0); + const char *zId = db_column_text(q, 1); + const char *zDate = db_column_text(q, 2); + const char *zCom = db_column_text(q, 3); + int nChild = db_column_int(q, 4); + int nParent = db_column_int(q, 5); + const char *zBranch = db_column_text(q, 7); + const char *zType = db_column_text(q, 8); + const char *zComShort = db_column_text(q, 9); + const char *zUserShort = db_column_text(q, 10); + const char *zTags = db_column_text(q, 11); + char *zFree = 0; + int n = 0; + char zPrefix[80]; + + if( nAbsLimit!=0 ){ + if( nLimit<0 && nLine>=nAbsLimit ){ + if( !g.fQuiet ){ + fossil_print("--- line limit (%d) reached ---\n", nAbsLimit); + } + break; /* line count limit hit, stop. */ + }else if( nEntry>=nAbsLimit ){ + if( !g.fQuiet ){ + fossil_print("--- entry limit (%d) reached ---\n", nAbsLimit); + } + break; /* entry count limit hit, stop. */ + } + } + if( zFormat == 0 && fossil_strnicmp(zDate, zPrevDate, 10) ){ + fossil_print("=== %.10s ===\n", zDate); + memcpy(zPrevDate, zDate, 10); + nLine++; /* record another line */ + } + if( zCom==0 ) zCom = ""; + if( zFormat == 0 ) + fossil_print("%.8s ", &zDate[11]); + zPrefix[0] = 0; + if( nParent>1 ){ + sqlite3_snprintf(sizeof(zPrefix), zPrefix, "*MERGE* "); + n = strlen(zPrefix); + } + if( nChild>1 ){ + const char *zBrType; + if( count_nonbranch_children(rid)>1 ){ + zBrType = "*FORK* "; + }else{ + zBrType = "*BRANCH* "; + } + sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], zBrType); + n = strlen(zPrefix); + } + if( fossil_strcmp(zCurrentUuid,zId)==0 ){ + sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*CURRENT* "); + n += strlen(zPrefix+n); + } + if( content_is_private(rid) ){ + sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*UNPUBLISHED* "); + n += strlen(zPrefix+n); + } + if( zType && zType[0]=='w' + && (zCom[0]=='+' || zCom[0]=='-' || zCom[0]==':') + ){ + /* Special processing for Wiki comments */ + if(!zComShort || !*zComShort){ + /* Shouldn't be possible, but just in case... */ + zComShort = " "; + } + if( zCom[0]=='+' ){ + zFree = mprintf("[%S] Add wiki page \"%s\" (user: %s)", + zId, zComShort+1, zUserShort); + }else if( zCom[0]=='-' ){ + zFree = mprintf("[%S] Delete wiki page \"%s\" (user: %s)", + zId, zComShort+1, zUserShort); + }else{ + zFree = mprintf("[%S] Edit to wiki page \"%s\" (user: %s)", + zId, zComShort+1, zUserShort); + } + }else{ + zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom); + + if( get_comment_format() & COMMENT_PRINT_SUMMARY ){ + char *z, *t; + for(z=zFree; *z!='\0'; z++){ + if( *z=='\n' ){ + for(t=z+1; *t!='\0' && *t!='\n' && fossil_isspace(*t); t++){} + if( *t=='\n' ) break; + } + } + *z = '\0'; + } + } + + if( zFormat ){ + char *zEntry; + int nEntryLine = 0; + if( nChild==0 ){ + sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*LEAF* "); + } + zEntry = timeline_entry_subst(zFormat, &nEntryLine, zId, zDate, + zUserShort, zComShort, zBranch, zTags, + zPrefix); + nLine += nEntryLine; + fossil_print("%s\n", zEntry); + fossil_free(zEntry); + } + else{ + /* record another X lines */ + nLine += comment_print(zFree, zCom, 9, width, get_comment_format()); + } + fossil_free(zFree); + + if(verboseFlag){ + if( !fchngQueryInit ){ + db_prepare(&fchngQuery, + "SELECT (pid<=0) AS isnew," + " (fid==0) AS isdel," + " (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name," + " (SELECT uuid FROM blob WHERE rid=fid)," + " (SELECT uuid FROM blob WHERE rid=pid)" + " FROM mlink" + " WHERE mid=:mid AND pid!=fid AND NOT mlink.isaux" + " ORDER BY 3 /*sort*/" + ); + fchngQueryInit = 1; + } + db_bind_int(&fchngQuery, ":mid", rid); + while( db_step(&fchngQuery)==SQLITE_ROW ){ + const char *zFilename = db_column_text(&fchngQuery, 2); + int isNew = db_column_int(&fchngQuery, 0); + int isDel = db_column_int(&fchngQuery, 1); + if( isNew ){ + fossil_print(" ADDED %s\n",zFilename); + }else if( isDel ){ + fossil_print(" DELETED %s\n",zFilename); + }else{ + fossil_print(" EDITED %s\n", zFilename); + } + nLine++; /* record another line */ + } + db_reset(&fchngQuery); + if( bVerboseNL ) fossil_print("\n"); + }else{ + if( bNoVerboseNL ) fossil_print("\n"); + } + + nEntry++; /* record another complete entry */ + } + if( rc==SQLITE_DONE ){ + /* Did the underlying query actually have all entries? */ + if( nAbsLimit==0 ){ + if( !g.fQuiet ){ + fossil_print("+++ end of timeline (%d) +++\n", nEntry); + } + }else{ + if( !g.fQuiet ){ + fossil_print("+++ no more data (%d) +++\n", nEntry); + } + } + } + if( fchngQueryInit ) db_finalize(&fchngQuery); +} + +/* +** wiki_to_text(TEXT) +** +** Return a text rendering of Fossil-Wiki TEXT, intended for display +** on a timeline. The timeline-plaintext and timeline-hard-newlines +** settings are considered when doing this rendering. +*/ +static void wiki_to_text_sqlfunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *zIn, *zOut; + int nIn, nOut; + Blob in, html, txt; + zIn = (const char*)sqlite3_value_text(argv[0]); + if( zIn==0 ) return; + nIn = sqlite3_value_bytes(argv[0]); + blob_init(&in, zIn, nIn); + blob_init(&html, 0, 0); + wiki_convert(&in, &html, wiki_convert_flags(0)); + blob_reset(&in); + blob_init(&txt, 0, 0); + html_to_plaintext(blob_str(&html), &txt, 0); + blob_reset(&html); + nOut = blob_size(&txt); + zOut = blob_str(&txt); + while( fossil_isspace(zOut[0]) ){ zOut++; nOut--; } + while( nOut>0 && fossil_isspace(zOut[nOut-1]) ){ nOut--; } + sqlite3_result_text(context, zOut, nOut, SQLITE_TRANSIENT); + blob_reset(&txt); +} + +/* +** Return a pointer to a static string that forms the basis for +** a timeline query for display on a TTY. +*/ +const char *timeline_query_for_tty(void){ + static int once = 0; + static const char zBaseSql[] = + @ SELECT + @ blob.rid AS rid, + @ uuid, + @ datetime(event.mtime,toLocal()) AS mDateTime, + @ wiki_to_text(coalesce(ecomment,comment)) + @ || ' (user: ' || coalesce(euser,user,'?') + @ || (SELECT case when length(x)>0 then ' tags: ' || x else '' end + @ FROM (SELECT group_concat(substr(tagname,5), ', ') AS x + @ FROM tag, tagxref + @ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid + @ AND tagxref.rid=blob.rid AND tagxref.tagtype>0)) + @ || ')' as comment, + @ (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim) + @ AS primPlinkCount, + @ (SELECT count(*) FROM plink WHERE cid=blob.rid) AS plinkCount, + @ event.mtime AS mtime, + @ tagxref.value AS branch, + @ event.type + @ , coalesce(ecomment,comment) AS comment0 + @ , coalesce(euser,user,'?') AS user0 + @ , (SELECT case when length(x)>0 then x else '' end + @ FROM (SELECT group_concat(substr(tagname,5), ', ') AS x + @ FROM tag, tagxref + @ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid + @ AND tagxref.rid=blob.rid AND tagxref.tagtype>0)) AS tags + @ FROM tag CROSS JOIN event CROSS JOIN blob + @ LEFT JOIN tagxref ON tagxref.tagid=tag.tagid + @ AND tagxref.tagtype>0 + @ AND tagxref.rid=blob.rid + @ WHERE blob.rid=event.objid + @ AND tag.tagname='branch' + ; + if( !once && g.db ){ + once = 1; + sqlite3_create_function(g.db, "wiki_to_text", 1, SQLITE_UTF8, 0, + wiki_to_text_sqlfunc, 0, 0); + } + return zBaseSql; +} + +/* +** Return true if the input string is a date in the ISO 8601 format: +** YYYY-MM-DD. +*/ +static int isIsoDate(const char *z){ + return strlen(z)==10 + && z[4]=='-' + && z[7]=='-' + && fossil_isdigit(z[0]) + && fossil_isdigit(z[5]); +} + +/* +** Return true if the input string can be converted to a Julian day. +*/ +static int fossil_is_julianday(const char *zDate){ + return db_int(0, "SELECT EXISTS (SELECT julianday(%Q) AS jd" + " WHERE jd IS NOT NULL)", zDate); +} + + +/* +** COMMAND: timeline +** +** Usage: %fossil timeline ?WHEN? ?CHECKIN|DATETIME? ?OPTIONS? +** +** Print a summary of activity going backwards in date and time +** specified or from the current date and time if no arguments +** are given. The WHEN argument can be any unique abbreviation +** of one of these keywords: +** +** before +** after +** descendants | children +** ancestors | parents +** +** The CHECKIN can be any unique prefix of 4 characters or more. You +** can also say "current" for the current version. +** +** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in +** year-month-day form, it may be truncated, the "T" may be replaced by +** a space, and it may also name a timezone offset from UTC as "-HH:MM" +** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" +** means UTC. +** +** +** Options: +** -b|--branch BRANCH Show only items on the branch named BRANCH +** -c|--current-branch Show only items on the current branch +** -F|--format Entry format. Values "oneline", "medium", and "full" +** get mapped to the full options below. Otherwise a +** string which can contain these placeholders: +** %n newline +** %% a raw % +** %H commit hash +** %h abbreviated commit hash +** %a author name +** %d date +** %c comment (NL, TAB replaced by space, LF erased) +** %b branch +** %t tags +** %p phase: zero or more of *CURRENT*, *MERGE*, +** *FORK*, *UNPUBLISHED*, *LEAF*, *BRANCH* +** --oneline Show only short hash and comment for each entry +** --medium Medium-verbose entry formatting +** --full Extra verbose entry formatting +** -n|--limit N If N is positive, output the first N entries. If +** N is negative, output the first -N lines. If N is +** zero, no limit. Default is -20 meaning 20 lines. +** --offset P Skip P changes +** -p|--path PATH Output items affecting PATH only. +** PATH can be a file or a subdirectory. +** -q|--quiet Do not print notifications at the end of the timeline. +** -r|--reverse Show items in chronological order. +** -R REPO_FILE Specifies the repository db to use. Default is +** the current check-out's repository. +** -s|--summmary Truncate comments after the first blank line. +** --sql Show the SQL used to generate the timeline +** -t|--type TYPE Output items from the given types only, such as: +** ci = file commits only +** e = technical notes only +** f = forum posts only +** t = tickets only +** w = wiki commits only +** -u|--for-user USER Only show items associated with USER +** -v|--verbose Output the list of files changed by each commit +** and the type of each change (edited, deleted, +** etc.) after the check-in comment. +** -W|--width N Width of lines (default is to auto-detect). N must be +** either greater than 20 or it must be zero 0 to +** indicate no limit, resulting in a single line per +** entry. +*/ +void timeline_cmd(void){ + Stmt q; + int n, k, width; + const char *zLimit; + const char *zWidth; + const char *zOffset; + const char *zType; + const char *zUser; + char *zOrigin; + char *zDate; + Blob sql; + int objid = 0; + Blob uuid; + int mode = TIMELINE_MODE_NONE; + int verboseFlag = 0; + int reverseFlag = 0; + int iOffset; + const char *zFilePattern = 0; + const char *zFormat = 0; + const char *zBr = 0; + Blob treeName; + int showSql = 0; + int bCommentGitStyle = 0; + int oldComFmtFlags = get_comment_format(); + + verboseFlag = find_option("verbose","v", 0)!=0; + if( !verboseFlag){ + verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */ + } + db_find_and_open_repository(0, 0); + zLimit = find_option("limit","n",1); + zWidth = find_option("width","W",1); + zType = find_option("type","t",1); + zUser = find_option("for-user","u",1); + zFilePattern = find_option("path","p",1); + zFormat = find_option("format","F",1); + if( (bCommentGitStyle = (find_option("summary","s",0))!=0) ){ + g.comFmtFlags |= COMMENT_PRINT_SUMMARY; + } + zBr = find_option("branch","b",1); + if( find_option("current-branch","c",0)!=0 ){ + if( !g.localOpen ){ + fossil_fatal("not within an open check-out"); + }else{ + int vid = db_lget_int("checkout", 0); + zBr = db_text(0, "SELECT value FROM tagxref WHERE rid=%d AND tagid=%d", + vid, TAG_BRANCH); + } + } + if( find_option("oneline",0,0)!= 0 || fossil_strcmp(zFormat,"oneline")==0 ){ + zFormat = TIMELINE_FMT_ONELINE; + } + if( find_option("medium",0,0)!= 0 || fossil_strcmp(zFormat,"medium")==0 ){ + zFormat = TIMELINE_FMT_MEDIUM; + } + if( find_option("full",0,0)!= 0 || fossil_strcmp(zFormat,"full")==0 ){ + zFormat = TIMELINE_FMT_FULL; + } + showSql = find_option("sql",0,0)!=0; + + if( !zLimit ){ + zLimit = find_option("count",0,1); + } + if( zLimit ){ + n = atoi(zLimit); + }else{ + n = -20; + } + if( zWidth ){ + width = atoi(zWidth); + if( (width!=0) && (width<=20) ){ + fossil_fatal("-W|--width value must be >20 or 0"); + } + }else{ + width = -1; + } + zOffset = find_option("offset",0,1); + iOffset = zOffset ? atoi(zOffset) : 0; + reverseFlag = find_option("reverse","r",0)!=0; + + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc>=4 ){ + k = strlen(g.argv[2]); + if( strncmp(g.argv[2],"before",k)==0 ){ + mode = TIMELINE_MODE_BEFORE; + }else if( strncmp(g.argv[2],"after",k)==0 && k>1 ){ + mode = TIMELINE_MODE_AFTER; + }else if( strncmp(g.argv[2],"descendants",k)==0 ){ + mode = TIMELINE_MODE_CHILDREN; + }else if( strncmp(g.argv[2],"children",k)==0 ){ + mode = TIMELINE_MODE_CHILDREN; + }else if( strncmp(g.argv[2],"ancestors",k)==0 && k>1 ){ + mode = TIMELINE_MODE_PARENTS; + }else if( strncmp(g.argv[2],"parents",k)==0 ){ + mode = TIMELINE_MODE_PARENTS; + }else if(!zType && !zLimit){ + usage("?WHEN? ?CHECKIN|DATETIME? ?-n|--limit #? ?-t|--type TYPE? " + "?-W|--width WIDTH? ?-p|--path PATH? ?-r|--reverse?"); + } + if( '-' != *g.argv[3] ){ + zOrigin = g.argv[3]; + }else{ + zOrigin = "now"; + } + }else if( g.argc==3 ){ + zOrigin = g.argv[2]; + }else{ + zOrigin = "now"; + } + k = strlen(zOrigin); + blob_zero(&uuid); + blob_append(&uuid, zOrigin, -1); + if( fossil_strcmp(zOrigin, "now")==0 ){ + if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ + fossil_fatal("cannot compute descendants or ancestors of a date"); + } + zDate = mprintf("(SELECT datetime('now'))"); + }else if( strncmp(zOrigin, "current", k)==0 ){ + if( !g.localOpen ){ + fossil_fatal("must be within a local check-out to use 'current'"); + } + objid = db_lget_int("checkout",0); + zDate = mprintf("(SELECT mtime FROM plink WHERE cid=%d)", objid); + }else if( fossil_is_julianday(zOrigin) ){ + const char *zShift = ""; + if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ + fossil_fatal("cannot compute descendants or ancestors of a date"); + } + if( mode==TIMELINE_MODE_NONE ){ + if( isIsoDate(zOrigin) ) zShift = ",'+1 day'"; + } + zDate = mprintf("(SELECT julianday(%Q%s, fromLocal()))", zOrigin, zShift); + }else if( name_to_uuid(&uuid, 0, "*")==0 ){ + objid = db_int(0, "SELECT rid FROM blob WHERE uuid=%B", &uuid); + zDate = mprintf("(SELECT mtime FROM event WHERE objid=%d)", objid); + }else{ + fossil_fatal("unknown check-in or invalid date: %s", zOrigin); + } + + if( zFilePattern ){ + if( zType==0 ){ + /* When zFilePattern is specified and type is not specified, only show + * file check-ins */ + zType="ci"; + } + file_tree_name(zFilePattern, &treeName, 0, 1); + if( fossil_strcmp(blob_str(&treeName), ".")==0 ){ + /* When zTreeName refers to g.zLocalRoot, it's like not specifying + * zFilePattern. */ + zFilePattern = 0; + } + } + + if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE; + blob_zero(&sql); + if( mode==TIMELINE_MODE_AFTER ){ + /* Extra outer select to get older rows in reverse order */ + blob_append(&sql, "SELECT *\nFROM (", -1); + } + blob_append(&sql, timeline_query_for_tty(), -1); + blob_append_sql(&sql, "\n AND event.mtime %s %s", + ( mode==TIMELINE_MODE_BEFORE || + mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/ + ); + if( zType && (zType[0]!='a') ){ + blob_append_sql(&sql, "\n AND event.type=%Q ", zType); + } + if( zUser && (zUser[0]!='\0') ){ + blob_append_sql(&sql, "\n AND user0=%Q ", zUser); + } + + /* When zFilePattern is specified, compute complete ancestry; + * limit later at print_timeline() */ + if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ + db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)"); + if( mode==TIMELINE_MODE_CHILDREN ){ + compute_descendants(objid, (zFilePattern ? 0 : n)); + }else{ + compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0); + } + blob_append_sql(&sql, "\n AND blob.rid IN ok"); + } + if( zFilePattern ){ + blob_append(&sql, + "\n AND EXISTS(SELECT 1 FROM mlink\n" + " WHERE mlink.mid=event.objid\n" + " AND mlink.fnid IN ", -1); + if( filenames_are_case_sensitive() ){ + blob_append_sql(&sql, + "(SELECT fnid FROM filename" + " WHERE name=%Q" + " OR name GLOB '%q/*')", + blob_str(&treeName), blob_str(&treeName)); + }else{ + blob_append_sql(&sql, + "(SELECT fnid FROM filename" + " WHERE name=%Q COLLATE nocase" + " OR lower(name) GLOB lower('%q/*'))", + blob_str(&treeName), blob_str(&treeName)); + } + blob_append(&sql, ")", -1); + } + if( zBr ){ + blob_append_sql(&sql, + "\n AND blob.rid IN (\n" /* Commits */ + " SELECT rid FROM tagxref NATURAL JOIN tag\n" + " WHERE tagtype>0 AND tagname='sym-%q'\n" + " UNION\n" /* Tags */ + " SELECT srcid FROM tagxref WHERE origid IN (\n" + " SELECT rid FROM tagxref NATURAL JOIN tag\n" + " WHERE tagname='sym-%q')\n" + " UNION\n" /* Branch wikis */ + " SELECT objid FROM event WHERE comment LIKE '_branch/%q'\n" + " UNION\n" /* Check-in wikis */ + " SELECT e.objid FROM event e\n" + " INNER JOIN blob b ON b.uuid=substr(e.comment, 10)\n" + " AND e.comment LIKE '_checkin/%%'\n" + " LEFT JOIN tagxref tx ON tx.rid=b.rid AND tx.tagid=%d\n" + " WHERE tx.value='%q'\n" + ")\n" /* No merge closures */ + " AND (tagxref.value IS NULL OR tagxref.value='%q')", + zBr, zBr, zBr, TAG_BRANCH, zBr, zBr); + } + + if( mode==TIMELINE_MODE_AFTER ){ + int lim = n; + if( n == 0 ){ + lim = -1; /* 0 means no limit */ + }else if( n < 0 ){ + lim = -n; + } + /* Complete the above outer select. */ + blob_append_sql(&sql, + "\nORDER BY event.mtime LIMIT %d) t ORDER BY t.mDateTime %s", + lim, reverseFlag ? "" : "DESC"); + }else{ + blob_append_sql(&sql, + "\nORDER BY event.mtime %s", reverseFlag ? "" : "DESC"); + } + if( iOffset>0 ){ + /* Don't handle LIMIT here, otherwise print_timeline() + * will not determine the end-marker correctly! */ + blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset); + } + if( showSql ){ + fossil_print("%s\n", blob_str(&sql)); + } + db_prepare_blob(&q, &sql); + blob_reset(&sql); + print_timeline(&q, n, width, zFormat, verboseFlag); + db_finalize(&q); + g.comFmtFlags = oldComFmtFlags; +} + +/* +** WEBPAGE: thisdayinhistory +** +** Generate a vanity page that shows project activity for the current +** day of the year for various years in the history of the project. +** +** Query parameters: +** +** today=DATE Use DATE as today's date +*/ +void thisdayinhistory_page(void){ + static int aYearsAgo[] = { 1,2,3,4,5,10,15,20,25,30,40,50,75,100 }; + const char *zToday; + char *zStartOfProject; + int i; + Stmt q; + char *z; + int bZulu = 0; + + login_check_credentials(); + if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum) ){ + login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); + return; + } + style_set_current_feature("timeline"); + style_header("Today In History"); + zToday = (char*)P("today"); + if( zToday ){ + zToday = timeline_expand_datetime(zToday, &bZulu); + if( !fossil_isdate(zToday) ) zToday = 0; + } + if( zToday==0 ){ + zToday = db_text(0, "SELECT date('now',toLocal())"); + } + @

This Day In History For %h(zToday)

+ z = db_text(0, "SELECT date(%Q,'-1 day')", zToday); + style_submenu_element("Yesterday", "%R/thisdayinhistory?today=%t", z); + z = db_text(0, "SELECT date(%Q,'+1 day')", zToday); + style_submenu_element("Tomorrow", "%R/thisdayinhistory?today=%t", z); + zStartOfProject = db_text(0, + "SELECT datetime(min(mtime),toLocal(),'startofday') FROM event;" + ); + timeline_temp_table(); + db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/"); + for(i=0; i<(int)(sizeof(aYearsAgo)/sizeof(aYearsAgo[0])); i++){ + int iAgo = aYearsAgo[i]; + char *zThis = db_text(0, "SELECT date(%Q,'-%d years')", zToday, iAgo); + Blob sql; + char *zId; + if( strcmp(zThis, zStartOfProject)<0 ) break; + blob_init(&sql, 0, 0); + blob_append(&sql, "INSERT OR IGNORE INTO timeline ", -1); + blob_append(&sql, timeline_query_for_www(), -1); + blob_append_sql(&sql, + " AND %Q=date(event.mtime,toLocal()) " + " AND event.mtime BETWEEN julianday(%Q,'-1 day')" + " AND julianday(%Q,'+2 days')", + zThis, zThis, zThis + ); + db_multi_exec("DELETE FROM timeline; %s;", blob_sql_text(&sql)); + blob_reset(&sql); + if( db_int(0, "SELECT count(*) FROM timeline")==0 ){ + continue; + } + zId = db_text(0, "SELECT timestamp FROM timeline" + " ORDER BY sortby DESC LIMIT 1"); + @

%d(iAgo) Year%s(iAgo>1?"s":"") Ago + @ %z(href("%R/timeline?c=%t",zId))(more context)\ + @

+ www_print_timeline(&q, TIMELINE_GRAPH, 0); + } + db_finalize(&q); + style_finish_page(); +} + + +/* +** COMMAND: test-timewarp-list +** +** Usage: %fossil test-timewarp-list ?-v|---verbose? +** +** Display all instances of child check-ins that appear earlier in time +** than their parent. If the -v|--verbose option is provided, both the +** parent and child check-ins and their times are shown. +*/ +void test_timewarp_cmd(void){ + Stmt q; + int verboseFlag; + + db_find_and_open_repository(0, 0); + verboseFlag = find_option("verbose", "v", 0)!=0; + if( !verboseFlag ){ + verboseFlag = find_option("detail", 0, 0)!=0; /* deprecated */ + } + db_prepare(&q, + "SELECT (SELECT uuid FROM blob WHERE rid=p.cid)," + " (SELECT uuid FROM blob WHERE rid=c.cid)," + " datetime(p.mtime), datetime(c.mtime)" + " FROM plink p, plink c" + " WHERE p.cid=c.pid AND p.mtime>c.mtime" + ); + while( db_step(&q)==SQLITE_ROW ){ + if( !verboseFlag ){ + fossil_print("%s\n", db_column_text(&q, 1)); + }else{ + fossil_print("%.14s -> %.14s %s -> %s\n", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2), + db_column_text(&q, 3)); + } + } + db_finalize(&q); +} + +/* +** WEBPAGE: timewarps +** +** Show all check-ins that are "timewarps". A timewarp is a +** check-in that occurs before its parent, according to the +** timestamp information on the check-in. This can only actually +** happen, of course, if a users system clock is set incorrectly. +*/ +void test_timewarp_page(void){ + Stmt q; + int cnt = 0; + + login_check_credentials(); + if( !g.perm.Read || !g.perm.Hyperlink ){ + login_needed(g.anon.Read && g.anon.Hyperlink); + return; + } + style_header("Instances of timewarp"); + db_prepare(&q, + "SELECT blob.uuid, " + " date(ce.mtime)," + " pe.mtime>ce.mtime," + " coalesce(ce.euser,ce.user)" + " FROM plink p, plink c, blob, event pe, event ce" + " WHERE p.cid=c.pid AND p.mtime>c.mtime" + " AND blob.rid=c.cid" + " AND pe.objid=p.cid" + " AND ce.objid=c.cid" + " ORDER BY 2 DESC" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zCkin = db_column_text(&q, 0); + const char *zDate = db_column_text(&q, 1); + const char *zStatus = db_column_int(&q,2) ? "Open" + : "Resolved by editing date"; + const char *zUser = db_column_text(&q, 3); + char *zHref = href("%R/timeline?c=%S", zCkin); + if( cnt==0 ){ + style_table_sorter(); + @
+ @ + @ + @ + @ + @ + @ + @ + } + @ + @ + @ + @ + @ + @ + fossil_free(zHref); + cnt++; + } + db_finalize(&q); + if( cnt==0 ){ + @

No timewarps in this repository

+ }else{ + @
Check-inDateUserStatus
%s(zHref)%S(zCkin)%s(zHref)%s(zDate)%h(zUser)%s(zStatus)
+ } + style_finish_page(); +} -- cgit v1.3.1