/*
** 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 julianday 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
/*
** 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 supplimental 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_VIEWS 0x001f000 /* 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 */
#endif
/*
** Hash a string and use the hash to determine a background color.
*/
char *hash_color(const char *z){
int i; /* Loop counter */
unsigned int h = 0; /* Hash on the branch name */
int r, g, b; /* Values for red, green, and blue */
int h1, h2, h3, h4; /* Elements of the hash value */
int mx, mn; /* Components of HSV */
static char zColor[10]; /* The resulting color */
static int ix[2] = {0,0}; /* Color chooser parameters */
if( ix[0]==0 ){
if( skin_detail_boolean("white-foreground") ){
ix[0] = 140;
ix[1] = 40;
}else{
ix[0] = 216;
ix[1] = 16;
}
}
for(i=0; z[i]; i++ ){
h = (h<<11) ^ (h<<1) ^ (h>>3) ^ z[i];
}
h1 = h % 6; h /= 6;
h3 = h % 30; h /= 30;
h4 = h % 40; h /= 40;
mx = ix[0] - h3;
mn = mx - h4 - ix[1];
h2 = (h%(mx - mn)) + mn;
switch( h1 ){
case 0: r = mx; g = h2, b = mn; break;
case 1: r = h2; g = mx, b = mn; break;
case 2: r = mn; g = mx, b = h2; break;
case 3: r = mn; g = h2, b = mx; break;
case 4: r = h2; g = mn, b = mx; break;
default: r = mx; g = mn, b = h2; break;
}
sqlite3_snprintf(8, zColor, "#%02x%02x%02x", r,g,b);
return zColor;
}
/*
** COMMAND: test-hash-color
**
** Usage: %fossil test-hash-color TAG ...
**
** Print out the color names associated with each tag. Used for
** testing the hash_color() function.
*/
void test_hash_color(void){
int i;
for(i=2; i
@ %h(zBr) - %s(hash_color(zBr)) -
@ Omnes nos quasi oves erravimus unusquisque in viam
@ suam declinavit.
cnt++;
}
}
if( cnt ){
@
}
@
style_finish_page("test");
}
/*
** 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;
}
/*
** 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 */
const char *zThisUser, /* Suppress links to this user */
const char *zThisTag, /* Suppress links to this tag */
const char *zLeftBranch, /* Strive to put this branch on the left margin */
int selectedRid, /* Highlight the line with this RID value or zero */
int secondRid, /* Secondary highlight (or zero) */
void (*xExtra)(int) /* Routine to call on each line of display */
){
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 */
static Stmt qbranch;
int pendingEndTr = 0; /* True if a is needed */
int vid = 0; /* Current checkout 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 */
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_VERBOSE ){
zStyle = "Verbose";
}else if( tmFlags & TIMELINE_CLASSIC ){
zStyle = "Classic";
}else{
zStyle = "Modern";
}
zDateFmt = P("datefmt");
if( zDateFmt ) dateFormat = atoi(zDateFmt);
if( tmFlags & TIMELINE_GRAPH ){
pGraph = graph_init();
}
db_static_prepare(&qbranch,
"SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
TAG_BRANCH
);
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);
const char *zTagList = db_column_text(pQuery, 8);
int tagid = db_column_int(pQuery, 9);
const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
const 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 isSelectedOrCurrent = 0; /* True if current row is selected */
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);
@
@ %s(zPrevDate)
@ | | |
}
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==selectedRid ){
@
isSelectedOrCurrent = 1;
}else if( rid==secondRid ){
@
isSelectedOrCurrent = 1;
}else if( rid==vid ){
@
isSelectedOrCurrent = 1;
}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("");
}
@ %z(zDateLink)%s(zTime) |
@
if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA) ){
if( tmFlags & TIMELINE_UCOLOR ){
zBgClr = zUser ? hash_color(zUser) : 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);
}
}
if( zType[0]=='c'
&& (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
){
db_reset(&qbranch);
db_bind_int(&qbranch, ":rid", rid);
if( db_step(&qbranch)==SQLITE_ROW ){
zBr = db_column_text(&qbranch, 0);
}else{
zBr = "trunk";
}
if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){
if( tmFlags & TIMELINE_DELTA ){
}else if( zBr==0 || strcmp(zBr,"trunk")==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;
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);
@
}
@ |
if( !isSelectedOrCurrent ){
@
}
if( pGraph ){
graph_finish(pGraph, zLeftBranch, tmFlags);
if( pGraph->nErr ){
graph_free(pGraph);
pGraph = 0;
}else{
@ \
@ | | |
}
}
@
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 */
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;
}
/*
** Convert a symbolic name used as an argument to the a=, b=, or c=
** query parameters of timeline into a julianday mtime value.
*/
double symbolic_name_to_mtime(const char *z, const char **pzDisplay){
double mtime;
int rid;
const char *zDate;
if( z==0 ) return -1.0;
if( fossil_isdate(z) ){
mtime = db_double(0.0, "SELECT julianday(%Q,fromLocal())", z);
if( mtime>0.0 ) return mtime;
}
zDate = fossil_expand_datetime(z, 1);
if( zDate!=0 ){
mtime = db_double(0.0, "SELECT julianday(%Q,fromLocal())",
fossil_roundup_date(zDate));
if( mtime>0.0 ){
if( pzDisplay ) *pzDisplay = fossil_strdup(zDate);
return mtime;
}
}
rid = symbolic_name_to_rid(z, "*");
if( rid ){
mtime = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
}else{
mtime = db_double(-1.0,
"SELECT max(event.mtime) FROM event, tag, tagxref"
" WHERE tag.tagname GLOB 'event-%q*'"
" AND tagxref.tagid=tag.tagid AND tagxref.tagtype"
" AND event.objid=tagxref.rid",
z
);
}
return mtime;
}
/*
** 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[16];
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";
}
assert( i<=count(az) );
}
if( i>2 ){
style_submenu_multichoice("y", i/2, az, isDisabled);
}
}
/*
** 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;
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",
"v", "Verbose View",
"x", "Classic View",
};
#if INTERFACE
# define N_TIMELINE_VIEW_STYLE 5
#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 || zChng[0]==0 ) return;
blob_append_sql(pSql," AND event.objid IN ("
"SELECT mlink.mid FROM mlink, filename"
" WHERE mlink.fnid=filename.fnid 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 || zChng[0]==0 ) return;
blob_appendf(pDescription, " that include changes to files matching '%h'",
zChng);
}
/*
** Tag match expression type code.
*/
typedef enum {
MS_EXACT, /* Matches a single tag by exact string comparison. */
MS_GLOB, /* Matches tags against a list of GLOB patterns. */
MS_LIKE, /* Matches tags against a list of LIKE patterns. */
MS_REGEXP /* Matches tags against a list of regular expressions. */
} MatchStyle;
/*
** Quote a tag string by surrounding it with double quotes and preceding
** internal double quotes and backslashes with backslashes.
*/
static const char *tagQuote(
int len, /* Maximum length of zTag, or negative for unlimited */
const char *zTag /* Tag string */
){
Blob blob = BLOB_INITIALIZER;
int i, j;
blob_zero(&blob);
blob_append(&blob, "\"", 1);
for( i=j=0; zTag[j] && (len<0 || ji ){
blob_append(&blob, zTag+i, j-i);
}
blob_append(&blob, "\\", 1);
i = j;
}
}
if( j>i ){
blob_append(&blob, zTag+i, j-i);
}
blob_append(&blob, "\"", 1);
return blob_str(&blob);
}
/*
** Construct the tag match SQL expression.
**
** This function is adapted from glob_expr() to support the MS_EXACT, MS_GLOB,
** MS_LIKE, and MS_REGEXP match styles. For MS_EXACT, the returned expression
** checks for integer match against the tag ID which is looked up directly by
** this function. For the other modes, the returned SQL expression performs
** string comparisons against the tag names, so it is necessary to join against
** the tag table to access the "tagname" column.
**
** Each pattern is adjusted to to start with "sym-" and be anchored at end.
**
** In MS_REGEXP mode, backslash can be used to protect delimiter characters.
** The backslashes are not removed from the regular expression.
**
** In addition to assembling and returning an SQL expression, this function
** makes an English-language description of the patterns being matched, suitable
** for display in the web interface.
**
** If any errors arise during processing, *zError is set to an error message.
** Otherwise it is set to NULL.
*/
static const char *tagMatchExpression(
MatchStyle matchStyle, /* Match style code */
const char *zTag, /* Tag name, match pattern, or pattern list */
const char **zDesc, /* Output expression description string */
const char **zError /* Output error string */
){
Blob expr = BLOB_INITIALIZER; /* SQL expression string assembly buffer */
Blob desc = BLOB_INITIALIZER; /* English description of match patterns */
Blob err = BLOB_INITIALIZER; /* Error text assembly buffer */
const char *zStart; /* Text at start of expression */
const char *zDelimiter; /* Text between expression terms */
const char *zEnd; /* Text at end of expression */
const char *zPrefix; /* Text before each match pattern */
const char *zSuffix; /* Text after each match pattern */
const char *zIntro; /* Text introducing pattern description */
const char *zPattern = 0; /* Previous quoted pattern */
const char *zFail = 0; /* Current failure message or NULL if okay */
const char *zOr = " or "; /* Text before final quoted pattern */
char cDel; /* Input delimiter character */
int i; /* Input match pattern length counter */
/* Optimize exact matches by looking up the ID in advance to create a simple
* numeric comparison. Bypass the remainder of this function. */
if( matchStyle==MS_EXACT ){
*zDesc = tagQuote(-1, zTag);
return mprintf("(tagid=%d)", db_int(-1,
"SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag));
}
/* Decide pattern prefix and suffix strings according to match style. */
if( matchStyle==MS_GLOB ){
zStart = "(";
zDelimiter = " OR ";
zEnd = ")";
zPrefix = "tagname GLOB 'sym-";
zSuffix = "'";
zIntro = "glob pattern ";
}else if( matchStyle==MS_LIKE ){
zStart = "(";
zDelimiter = " OR ";
zEnd = ")";
zPrefix = "tagname LIKE 'sym-";
zSuffix = "'";
zIntro = "SQL LIKE pattern ";
}else/* if( matchStyle==MS_REGEXP )*/{
zStart = "(tagname REGEXP '^sym-(";
zDelimiter = "|";
zEnd = ")$')";
zPrefix = "";
zSuffix = "";
zIntro = "regular expression ";
}
/* Convert the list of matches into an SQL expression and text description. */
blob_zero(&expr);
blob_zero(&desc);
blob_zero(&err);
while( 1 ){
/* Skip leading delimiters. */
for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag );
/* Next non-delimiter character determines quoting. */
if( !*zTag ){
/* Terminate loop at end of string. */
break;
}else if( *zTag=='\'' || *zTag=='"' ){
/* If word is quoted, prepare to stop at end quote. */
cDel = *zTag;
++zTag;
}else{
/* If word is not quoted, prepare to stop at delimiter. */
cDel = ',';
}
/* Find the next delimiter character or end of string. */
for( i=0; zTag[i] && zTag[i]!=cDel; ++i ){
/* If delimiter is comma, also recognize spaces as delimiters. */
if( cDel==',' && fossil_isspace(zTag[i]) ){
break;
}
/* In regexp mode, ignore delimiters following backslashes. */
if( matchStyle==MS_REGEXP && zTag[i]=='\\' && zTag[i+1] ){
++i;
}
}
/* Check for regular expression syntax errors. */
if( matchStyle==MS_REGEXP ){
ReCompiled *regexp;
char *zTagDup = fossil_strndup(zTag, i);
zFail = re_compile(®exp, zTagDup, 0);
re_free(regexp);
fossil_free(zTagDup);
}
/* Process success and error results. */
if( !zFail ){
/* Incorporate the match word into the output expression. %q is used to
* protect against SQL injection attacks by replacing ' with ''. */
blob_appendf(&expr, "%s%s%#q%s", blob_size(&expr) ? zDelimiter : zStart,
zPrefix, i, zTag, zSuffix);
/* Build up the description string. */
if( !blob_size(&desc) ){
/* First tag: start with intro followed by first quoted tag. */
blob_append(&desc, zIntro, -1);
blob_append(&desc, tagQuote(i, zTag), -1);
}else{
if( zPattern ){
/* Third and subsequent tags: append comma then previous tag. */
blob_append(&desc, ", ", 2);
blob_append(&desc, zPattern, -1);
zOr = ", or ";
}
/* Second and subsequent tags: store quoted tag for next iteration. */
zPattern = tagQuote(i, zTag);
}
}else{
/* On error, skip the match word and build up the error message buffer. */
if( !blob_size(&err) ){
blob_append(&err, "Error: ", 7);
}else{
blob_append(&err, ", ", 2);
}
blob_appendf(&err, "(%s%s: %s)", zIntro, tagQuote(i, zTag), zFail);
}
/* Advance past all consumed input characters. */
zTag += i;
if( cDel!=',' && *zTag==cDel ){
++zTag;
}
}
/* Finalize and extract the pattern description. */
if( zPattern ){
blob_append(&desc, zOr, -1);
blob_append(&desc, zPattern, -1);
}
*zDesc = blob_str(&desc);
/* Finalize and extract the error text. */
*zError = blob_size(&err) ? blob_str(&err) : 0;
/* Finalize and extract the SQL expression. */
if( blob_size(&expr) ){
blob_append(&expr, zEnd, -1);
return blob_str(&expr);
}
/* If execution reaches this point, the pattern was empty. Return NULL. */
return 0;
}
/*
** 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){
static char zEDate[20];
static const char aPunct[] = { 0, 0, '-', '-', ' ', ':', ':' };
int n = (int)strlen(zIn);
int i, j;
/* Only three forms allowed:
** (1) YYYYMMDD
** (2) YYYYMM
** (3) YYYYWW
*/
if( n!=8 && n!=6 ) return zIn;
/* Every character must be a digit */
for(i=0; fossil_isdigit(zIn[i]); i++){}
if( i!=n ) return zIn;
/* Expand the date */
for(i=j=0; zIn[i]; i++){
if( i>=4 && (i%2)==0 ){
zEDate[j++] = aPunct[i/2];
}
zEDate[j++] = zIn[i];
}
zEDate[j] = 0;
/* It looks like this may be a date. Return it with punctuation added. */
return zEDate;
}
/*
** 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
** n=COUNT Maximum number of events. "all" for no limit
** p=CHECKIN Parents and ancestors of CHECKIN
** bt=PRIOR ... going back to PRIOR
** d=CHECKIN Children and descendants of CHECKIN
** dp=CHECKIN The same as 'd=CHECKIN&p=CHECKIN'
** t=TAG Show only check-ins with the given TAG
** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
** rel Show related check-ins as well as those matching t=TAG
** mionly Limit rel to show ancestors but not descendants
** nowiki Do not show wiki associated with branch or tag
** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
** u=USER Only show items associated with USER
** y=TYPE 'ci', 'w', 't', 'n', 'e', 'f', 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
** v Show details of files changed
** vfx Show complete text of forum messages
** f=CHECKIN Show family (immediate parents and children) of CHECKIN
** from=CHECKIN Path from...
** to=CHECKIN ... to this
** shortest ... show only the shortest path
** rel ... also show related checkins
** uf=FILE_HASH Show only check-ins that contain the given file version
** chng=GLOBLIST Show only check-ins that involve changes to a file whose
** name matches one of the comma-separate 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=YYYY-MM Show only events for the given year/month
** yw=YYYY-WW Show only events for the given week of the given year
** yw=YYYY-MM-DD Show events for the week that includes the given day
** ymd=YYYY-MM-DD Show only events on the given day. The use "ymd=now"
** to see all changes for the current week.
** 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
** showid Show RIDs
** showsql Show the SQL text
**
** 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.
**
** 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 = name_to_typed_rid(P("p"),"ci"); /* artifact p and its parents */
int d_rid = name_to_typed_rid(P("d"),"ci"); /* artifact d and descendants */
int f_rid = name_to_typed_rid(P("f"),"ci"); /* 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 *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 checkins */
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 */
int to_rid = name_to_typed_rid(P("to"),"ci"); /* to= for path timelines */
int noMerge = P("shortest")==0; /* Follow merge links if shorter */
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;
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 */
url_initialize(&url, "timeline");
cgi_query_parameters_to_url(&url);
/* Set number of rows to display */
haveParameterN = P("n")!=0;
cookie_read_parameter("n","n");
z = P("n");
if( z==0 ) z = db_get("timeline-default-length",0);
if( z ){
if( fossil_strcmp(z,"all")==0 ){
nEntry = 0;
}else{
nEntry = atoi(z);
if( nEntry<=0 ){
z = "10";
nEntry = 10;
}
}
}else{
z = "50";
nEntry = 50;
}
/* 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");
cgi_replace_query_parameter("n",z);
cookie_write_parameter("n","n",0);
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_to_typed_rid(P("dp"),"ci");
if( pd_rid ){
p_rid = d_rid = pd_rid;
}
login_check_credentials();
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( !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' || zType[0]=='c' ){
cookie_write_parameter("y","y",zType);
}
cookie_render();
/* 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")
);
}
/* Convert r=TAG to t=TAG&rel in order to populate the UI style widgets. */
if( zBrName && !related ){
cgi_delete_query_parameter("r");
cgi_set_query_parameter("t", zBrName);
cgi_set_query_parameter("rel", "1");
zTagName = zBrName;
related = 1;
zType = "ci";
}
/* Ignore empty tag query strings. */
if( zTagName && !*zTagName ){
zTagName = 0;
}
/* Finish preliminary processing of tag match queries. */
if( zTagName ){
zType = "ci";
/* Interpet the tag style string. */
if( fossil_stricmp(zMatchStyle, "glob")==0 ){
matchStyle = MS_GLOB;
}else if( fossil_stricmp(zMatchStyle, "like")==0 ){
matchStyle = MS_LIKE;
}else if( fossil_stricmp(zMatchStyle, "regexp")==0 ){
matchStyle = MS_REGEXP;
}else{
/* For exact maching, inhibit links to the selected tag. */
zThisTag = zTagName;
Th_Store("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 = tagMatchExpression(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( zUses!=0 ){
int ufid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUses);
if( ufid ){
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;
}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)=="
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n"
" GROUP BY pid"
" HAVING count(*)>1;\n"
"INSERT OR IGNORE INTO rnfork(rid)"
" SELECT cid FROM plink\n"
" WHERE (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)=="
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n"
" GROUP BY cid"
" 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"
" AND (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)=="
" (SELECT value FROM tagxref WHERE tagid=%d AND rid=pid)\n"
" UNION "
" SELECT pid FROM plink\n"
" WHERE cid IN rnfork"
" AND (SELECT value FROM tagxref WHERE tagid=%d AND rid=cid)=="
" (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
&& fossil_strcmp(g.zIpAddr,"127.0.0.1")==0
&& 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?cmd=/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) || (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;
if( from_rid && to_rid ){
p = path_shortest(from_rid, to_rid, noMerge, 0, 0);
zFrom = P("from");
zTo = P("to");
}else{
if( path_common_ancestor(me_rid, you_rid) ){
p = path_first();
}
zFrom = P("me");
zTo = P("you");
}
blob_init(&ins, 0, 0);
db_multi_exec(
"CREATE TABLE IF NOT EXISTS temp.pathnode(x INTEGER PRIMARY KEY);"
);
if( p ){
blob_init(&ins, 0, 0);
blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
p = p->u.pTo;
while( p ){
blob_append_sql(&ins, ",(%d)", p->rid);
p = p->u.pTo;
}
}
path_reset();
db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
blob_reset(&ins);
if( related || P("mionly") ){
db_multi_exec(
"CREATE TABLE IF NOT EXISTS temp.related(x INTEGER PRIMARY KEY);"
"INSERT OR IGNORE INTO related(x)"
" SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;"
);
if( P("mionly")==0 ){
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( P("mionly")==0 ){
db_multi_exec(
"INSERT OR IGNORE INTO related(x)"
" SELECT childid FROM cherrypick WHERE parentid IN pathnode;"
);
}
}
db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related");
}
blob_append_sql(&sql, " AND event.objid IN pathnode");
if( zChng && zChng[0] ){
db_multi_exec(
"DELETE FROM pathnode "
" WHERE NOT EXISTS(SELECT 1 FROM mlink, filename"
" WHERE mlink.mid=x"
" AND mlink.fnid=filename.fnid 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");
blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
blob_appendf(&desc, "%z%h", href("%R/info/%h", zFrom), zFrom);
blob_append(&desc, " to ", -1);
blob_appendf(&desc, "%z%h", href("%R/info/%h",zTo), zTo);
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 p= or d= is present, ignore all other parameters other than n= */
char *zUuid;
const char *zCiName;
int np = 0, nd;
const char *zBackTo = 0;
int ridBackTo = 0;
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
if( p_rid && d_rid ){
if( p_rid!=d_rid ) p_rid = d_rid;
if( P("n")==0 ) nEntry = 10;
}
db_multi_exec(
"CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
);
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
p_rid ? p_rid : d_rid);
zCiName = pd_rid ? P("pd") : p_rid ? P("p") : P("d");
if( zCiName==0 ) zCiName = zUuid;
blob_append_sql(&sql, " AND event.objid IN ok");
nd = 0;
if( d_rid ){
compute_descendants(d_rid, nEntry==0 ? 0 : nEntry+1);
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,(1==nd)?"":"s");
}
if( useDividers ) selectedRid = d_rid;
db_multi_exec("DELETE FROM ok");
}
if( p_rid ){
zBackTo = P("bt");
ridBackTo = zBackTo ? name_to_typed_rid(zBackTo,"ci") : 0;
if( !haveParameterN ) nEntry = 0;
compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0, ridBackTo);
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, (1==np)?"":"s");
db_multi_exec("%s", blob_sql_text(&sql));
}
if( useDividers ) selectedRid = p_rid;
}
blob_appendf(&desc, " of %z%h",
href("%R/info?name=%h", zCiName), zCiName);
if( ridBackTo ){
if( np==0 ){
blob_reset(&desc);
blob_appendf(&desc,
"Check-in %z%h only (%z%h is not an ancestor)",
href("%R/info?name=%h",zCiName), zCiName,
href("%R/info?name=%h",zBackTo), zBackTo);
}else{
blob_appendf(&desc, " back to %z%h",
href("%R/info?name=%h",zBackTo), zBackTo);
}
}
if( d_rid ){
if( p_rid ){
/* If both p= and d= are set, we don't have the uuid of d yet. */
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", d_rid);
}
}
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 = 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 ");
}
if( renameOnly ){
blob_append_sql(&cond, " AND event.objid IN rnfile ");
}
if( forkOnly ){
blob_append_sql(&cond, " AND event.objid IN rnfork ");
}
if( cpOnly && showCherrypicks ){
db_multi_exec(
"CREATE 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 ");
}
if( bisectLocal || zBisect!=0 ){
blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
}
if( zYearMonth ){
char *zNext;
zYearMonth = timeline_expand_datetime(zYearMonth);
if( strlen(zYearMonth)>7 ){
zYearMonth = mprintf("%.7s", zYearMonth);
}
if( db_int(0,"SELECT julianday('%q-01') IS NULL", zYearMonth) ){
zYearMonth = db_text(0, "SELECT strftime('%%Y-%%m','now');");
}
zNext = db_text(0, "SELECT strftime('%%Y-%%m','%q-01','+1 month');",
zYearMonth);
if( db_int(0,
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
" WHERE blob.rid=event.objid AND mtime>=julianday('%q-01')%s)",
zNext, blob_sql_text(&cond))
){
zNewerButton = fossil_strdup(url_render(&url, "ym", zNext, 0, 0));
zNewerButtonLabel = "Following month";
}
fossil_free(zNext);
zNext = db_text(0, "SELECT strftime('%%Y-%%m','%q-01','-1 month');",
zYearMonth);
if( db_int(0,
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
" WHERE blob.rid=event.objid AND mtime=julianday(%Q)%s)",
zNext, blob_sql_text(&cond))
){
zNewerButton = fossil_strdup(url_render(&url, "yw", zNext, 0, 0));
zNewerButtonLabel = "Following week";
}
fossil_free(zNext);
zNext = db_text(0, "SELECT date(%Q,'-7 days');", zYearWeekStart);
if( db_int(0,
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
" WHERE blob.rid=event.objid AND mtime=julianday(%Q)%s)",
zNext, blob_sql_text(&cond))
){
zNewerButton = fossil_strdup(url_render(&url, "ymd", zNext, 0, 0));
zNewerButtonLabel = "Following day";
}
fossil_free(zNext);
zNext = db_text(0, "SELECT date(%Q,'-1 day');", zDay);
if( db_int(0,
"SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob"
" WHERE blob.rid=event.objid AND mtime=julianday('now','-%d days') ",
nDays);
nEntry = -1;
}
if( zTagSql ){
db_multi_exec(
"CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);"
"INSERT OR IGNORE INTO selected_nodes"
" SELECT tagxref.rid FROM tagxref NATURAL JOIN tag"
" WHERE %s AND tagtype>0", 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);
}
if( !related ){
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"
" SELECT pid FROM selected_nodes CROSS JOIN plink"
" WHERE selected_nodes.rid=plink.cid;"
);
if( P("mionly")==0 ){
db_multi_exec(
"INSERT OR IGNORE INTO related_nodes"
" SELECT cid FROM selected_nodes CROSS JOIN plink"
" WHERE selected_nodes.rid=plink.pid;"
);
if( showCherrypicks ){
db_multi_exec(
"INSERT OR IGNORE INTO related_nodes"
" SELECT childid FROM selected_nodes CROSS JOIN cherrypick"
" WHERE selected_nodes.rid=cherrypick.parentid;"
);
}
}
if( showCherrypicks ){
db_multi_exec(
"INSERT OR IGNORE INTO related_nodes"
" SELECT parentid FROM selected_nodes CROSS JOIN cherrypick"
" WHERE selected_nodes.rid=cherrypick.childid;"
);
}
if( (tmFlags & TIMELINE_UNHIDE)==0 ){
db_multi_exec(
"DELETE FROM related_nodes WHERE rid IN "
" (SELECT related_nodes.rid FROM related_nodes, tagxref"
" WHERE tagid=%d AND tagtype>0 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 = "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{
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 tickets";
}else if( zType[0]=='e' ){
zEType = "technical note";
}else if( zType[0]=='g' ){
zEType = "tag";
}else if( zType[0]=='f' ){
zEType = "forum post";
}
}
if( zUser ){
int n = db_int(0,"SELECT count(*) FROM event"
" WHERE user=%Q OR euser=%Q", zUser, zUser);
if( n<=nEntry ){
zCirca = zBefore = zAfter = 0;
nEntry = -1;
}
blob_append_sql(&cond, " AND (event.user=%Q OR event.euser=%Q)",
zUser, zUser);
zThisUser = zUser;
}
if( zSearch ){
blob_append_sql(&cond,
" AND (event.comment LIKE '%%%q%%' OR event.brief LIKE '%%%q%%')",
zSearch, zSearch);
}
rBefore = symbolic_name_to_mtime(zBefore, &zBefore);
rAfter = symbolic_name_to_mtime(zAfter, &zAfter);
rCirca = symbolic_name_to_mtime(zCirca, &zCirca);
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"
" ORDER BY event.mtime ASC", rAfter-ONE_SECOND, rBefore+ONE_SECOND);
nEntry = -1;
}else{
blob_append_sql(&sql,
" AND event.mtime>=%.17g 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 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 ORDER BY event.mtime ASC", rCirca);
if( nEntry>0 ){
blob_append_sql(&sql2," LIMIT %d", (nEntry+1)/2);
}
if( PB("showsql") ){
@ %h(blob_sql_text(&sql2))
}
db_multi_exec("%s", blob_sql_text(&sql2));
if( nEntry>0 ){
nEntry -= db_int(0,"select count(*) from timeline");
}
blob_reset(&sql2);
blob_append_sql(&sql,
" AND event.mtime<=%f 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-01",
n, zEType, zPlural, zYearMonth);
}else if( zYearWeek ){
blob_appendf(&desc, "%d %s%s for week %h beginning on %h",
n, zEType, zPlural, zYearWeek, zYearWeekStart);
}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 ){
blob_appendf(&desc, " by user %h", zUser);
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
}
if( zTagSql ){
if( matchStyle==MS_EXACT ){
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"
};
double rDate;
zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/");
if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){
zDate = mprintf("%s", (zAfter ? zAfter : zBefore));
}
if( zDate ){
rDate = symbolic_name_to_mtime(zDate, 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 = mprintf("%s", (zBefore ? zBefore : zAfter));
}
if( zDate ){
rDate = symbolic_name_to_mtime(zDate, 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( PB("showsql") ){
@ %h(blob_sql_text(&sql))
}
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);
if( r>0.0 ) selectedRid = timeline_add_divider(r);
}
blob_zero(&sql);
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)
}
if( zNewerButton ){
@ %z(chref("button","%s",zNewerButton))%h(zNewerButtonLabel)\
@ ↑
}
www_print_timeline(&q, tmFlags, zThisUser, zThisTag, zBrName,
selectedRid, secondaryRid, 0);
db_finalize(&q);
if( zOlderButton ){
@ %z(chref("button","%s",zOlderButton))%h(zOlderButtonLabel)\
@ ↓
}
document_emit_js(/*handles pikchrs rendered above*/);
style_finish_page("timeline");
}
/*
** 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 and user
** 4. Number of non-merge children
** 5. Number of parents
** 6. mtime
** 7. branch
** 8. event-type: 'ci', 'w', 't', 'f', and so forth.
*/
void print_timeline(Stmt *q, int nLimit, int width, 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;
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 *zType = db_column_text(q, 8);
char *zFree = 0;
int n = 0;
char zPrefix[80];
if( nAbsLimit!=0 ){
if( nLimit<0 && nLine>=nAbsLimit ){
fossil_print("--- line limit (%d) reached ---\n", nAbsLimit);
break; /* line count limit hit, stop. */
}else if( nEntry>=nAbsLimit ){
fossil_print("--- entry limit (%d) reached ---\n", nAbsLimit);
break; /* entry count limit hit, stop. */
}
}
if( fossil_strnicmp(zDate, zPrevDate, 10) ){
fossil_print("=== %.10s ===\n", zDate);
memcpy(zPrevDate, zDate, 10);
nLine++; /* record another line */
}
if( zCom==0 ) zCom = "";
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[0]=='w' && (zCom[0]=='+' || zCom[0]=='-' || zCom[0]==':') ){
/* Special processing for Wiki comments */
if( zCom[0]=='+' ){
zFree = mprintf("[%S] Add wiki page \"%s\"", zId, zCom+1);
}else if( zCom[0]=='-' ){
zFree = mprintf("[%S] Delete wiki page \"%s\"", zId, zCom+1);
}else{
zFree = mprintf("[%S] Edit to wiki page \"%s\"", zId, zCom+1);
}
}else{
zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom);
}
/* 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);
}
nEntry++; /* record another complete entry */
}
if( rc==SQLITE_DONE ){
/* Did the underlying query actually have all entries? */
if( nAbsLimit==0 ){
fossil_print("+++ end of timeline (%d) +++\n", nEntry);
}else{
fossil_print("+++ no more data (%d) +++\n", nEntry);
}
}
if( fchngQueryInit ) db_finalize(&fchngQuery);
}
/*
** 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 const char zBaseSql[] =
@ SELECT
@ blob.rid AS rid,
@ uuid,
@ datetime(event.mtime,toLocal()) AS mDateTime,
@ 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
@ 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'
;
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 julianday.
*/
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:
** -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.
** -p|--path PATH Output items affecting PATH only.
** PATH can be a file or a sub directory.
** --offset P skip P changes
** --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
** t = tickets only
** w = wiki commits only
** -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 ust be zero 0 to
** indicate no limit, resulting in a single line per
** entry.
** -R REPO_FILE Specifies the repository db to use. Default is
** the current checkout's repository.
*/
void timeline_cmd(void){
Stmt q;
int n, k, width;
const char *zLimit;
const char *zWidth;
const char *zOffset;
const char *zType;
char *zOrigin;
char *zDate;
Blob sql;
int objid = 0;
Blob uuid;
int mode = TIMELINE_MODE_NONE;
int verboseFlag = 0 ;
int iOffset;
const char *zFilePattern = 0;
Blob treeName;
int showSql = 0;
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);
zFilePattern = find_option("path","p",1);
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;
/* 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");
}
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 checkout 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);
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*/
);
/* 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( zType && (zType[0]!='a') ){
blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
}
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);
}
blob_append_sql(&sql, "\nORDER BY event.mtime 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, verboseFlag);
db_finalize(&q);
}
/*
** 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, 30, 40, 50, 75, 100 };
const char *zToday;
char *zStartOfProject;
int i;
Stmt q;
char *z;
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_header("Today In History");
zToday = (char*)P("today");
if( zToday ){
zToday = timeline_expand_datetime(zToday);
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%d(iAgo) Year%s(iAgo>1?"s":"") Ago
@ %z(href("%R/timeline?c=%t",zId))(more context)\
@
www_print_timeline(&q, TIMELINE_GRAPH, 0, 0, 0, 0, 0, 0);
}
db_finalize(&q);
style_finish_page("timeline");
}
/*
** 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();
@
@
@
@ Check-in |
@ Date |
@ User |
@ Status |
@
}
@
@ %s(zHref)%S(zCkin) |
@ %s(zHref)%s(zDate) |
@ %h(zUser) |
@ %s(zStatus) |
@
fossil_free(zHref);
cnt++;
}
db_finalize(&q);
if( cnt==0 ){
@ No timewarps in this repository
}else{
@
}
style_finish_page("timewarps");
}