From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/name.c | 2671 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2671 insertions(+) create mode 100644 src/name.c (limited to 'src/name.c') diff --git a/src/name.c b/src/name.c new file mode 100644 index 0000000..00ebe4f --- /dev/null +++ b/src/name.c @@ -0,0 +1,2671 @@ +/* +** Copyright (c) 2006 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 used to resolved user-supplied object names. +*/ +#include "config.h" +#include "name.h" +#include + +#if INTERFACE +/* +** An upper boundary on RIDs, provided in order to be able to +** distinguish real RID values from RID_CKOUT and any future +** RID_... values. +*/ +#define RID_MAX 0x7ffffff0 +/* +** A "magic" RID representing the current checkout in some contexts. +*/ +#define RID_CKOUT (RID_MAX+1) +#endif + +/* +** Return TRUE if the string begins with something that looks roughly +** like an ISO date/time string. The SQLite date/time functions will +** have the final say-so about whether or not the date/time string is +** well-formed. +*/ +int fossil_isdate(const char *z){ + if( !fossil_isdigit(z[0]) ) return 0; + if( !fossil_isdigit(z[1]) ) return 0; + if( !fossil_isdigit(z[2]) ) return 0; + if( !fossil_isdigit(z[3]) ) return 0; + if( z[4]!='-') return 0; + if( !fossil_isdigit(z[5]) ) return 0; + if( !fossil_isdigit(z[6]) ) return 0; + if( z[7]!='-') return 0; + if( !fossil_isdigit(z[8]) ) return 0; + if( !fossil_isdigit(z[9]) ) return 0; + return 1; +} + +/* +** Check to see if the string might be a compact date/time that omits +** the punctuation. Example: "20190327084549" instead of +** "2019-03-27 08:45:49". If the string is of the appropriate form, +** then return an alternative string (in static space) that is the same +** string with punctuation inserted. +** +** If the bRoundUp parameter is true, then round the resulting date-time +** up to the largest date/time that is consistent with the input value. +** This is because the result will be used for an mtime<=julianday($DATE) +** comparison. In other words: +** +** 20250317123421 -> 2025-03-17 12:34:21.999 +** ^^^^--- Added +** +** 202503171234 -> 2025-03-17 12:34:59.999 +** ^^^^^^^--- Added +** 20250317 -> 2025-03-17 23:59:59.999 +** ^^^^^^^^^^^^^--- Added +** +** If the bVerifyNotAHash flag is true, then a check is made to see if +** the input string is a hash prefix and NULL is returned if it is. If the +** bVerifyNotAHash flag is false, then the result is determined by syntax +** of the input string only, without reference to the artifact table. +*/ +char *fossil_expand_datetime(const char *zIn,int bVerifyNotAHash,int bRoundUp){ + static char zEDate[24]; + static const char aPunct[] = { 0, 0, '-', '-', ' ', ':', ':' }; + int n = (int)strlen(zIn); + int i, j; + int addZulu = 0; + + /* These forms are allowed: + ** + ** 123456789 1234 123456789 123456789 1234 + ** (1) YYYYMMDD => YYYY-MM-DD 23:59:59.999 + ** (2) YYYYMMDDHHMM => YYYY-MM-DD HH:MM:59.999 + ** (3) YYYYMMDDHHMMSS => YYYY-MM-DD HH:MM:SS.999 + ** + ** An optional "Z" zulu timezone designator is allowed at the end. + */ + if( n>0 && (zIn[n-1]=='Z' || zIn[n-1]=='z') ){ + n--; + addZulu = 1; + } + if( n!=8 && n!=12 && n!=14 ){ + return 0; + } + + /* Every character must be a digit */ + for(i=0; fossil_isdigit(zIn[i]); i++){} + if( i!=n && (!addZulu || i!=n+1) ) return 0; + + /* Expand the date */ + for(i=j=0; i=4 && (i%2)==0 ){ + zEDate[j++] = aPunct[i/2]; + } + zEDate[j++] = zIn[i]; + } + if( bRoundUp ){ + if( j==10 ){ + memcpy(&zEDate[10], " 23:59:59.999", 13); + j += 13; + }else if( j==16 ){ + memcpy(&zEDate[16], ":59.999",7); + j += 7; + }else if( j==19 ){ + memcpy(&zEDate[19], ".999", 4); + j += 4; + } + } + if( addZulu ){ + zEDate[j++] = 'Z'; + } + zEDate[j] = 0; + + /* Check for reasonable date values. + ** Offset references: + ** YYYY-MM-DD HH:MM:SS + ** 0123456789 12345678 + */ + + i = atoi(zEDate); + if( i<1970 || i>2100 ) return 0; + i = atoi(zEDate+5); + if( i<1 || i>12 ) return 0; + i = atoi(zEDate+8); + if( i<1 || i>31 ) return 0; + if( n>8 ){ + i = atoi(zEDate+11); + if( i>24 ) return 0; + i = atoi(zEDate+14); + if( i>60 ) return 0; + if( n==14 && atoi(zEDate+17)>60 ) return 0; + } + + /* The string is not also a hash prefix */ + if( bVerifyNotAHash && !addZulu ){ + if( db_exists("SELECT 1 FROM blob WHERE uuid GLOB '%q*'",zIn) ) return 0; + } + + /* It looks like this may be a date. Return it with punctuation added. */ + return zEDate; +} + +/* +** The data-time string in the argument is going to be used as an +** upper bound like this: mtime<=julianday(zDate,'localtime'). +** But if the zDate parameter omits the fractional seconds or the +** seconds, or the time, that might mess up the == part of the +** comparison. So add in missing factional seconds or seconds or time. +** +** The returned string is held in a static buffer that is overwritten +** with each call, or else is just a copy of its input if there are +** no changes. +** +** For reference: +** +** 0123456789 123456789 1234 +** YYYY-MM-DD HH:MM:SS.SSSz +*/ +const char *fossil_roundup_date(const char *zDate){ + static char zUp[28]; + int n = (int)strlen(zDate); + int addZ = 0; + if( n>10 && (zDate[n-1]=='z' || zDate[n-1]=='Z') ){ + n--; + addZ = 1; + } + if( n>=21 && n<=23 ){ /* YYYY-MM-DD HH:MM:SS.SSS */ + memcpy(zUp, zDate, n); + while( n<23 ) zUp[n++] = '9'; + /* If milliseconds is less than 999, round up to the next millisecond */ + if( strcmp(&zUp[20],"999")<0 ){ + if( zUp[22]<'9' ){ + zUp[22]++; + }else{ + zUp[22] = '0'; + if( zUp[21]<'9' ){ + zUp[21]++; + }else{ + zUp[21] = '0'; + zUp[20]++; + } + } + } + if( addZ ) zUp[n++] = 'z'; + zUp[n] = 0; + return zUp; + } + if( n==19 ){ /* YYYY-MM-DD HH:MM:SS */ + memcpy(zUp, zDate, 19); + memcpy(zUp+19, ".999z", 6); + if( !addZ ) zUp[23] = 0; + return zUp; + } + if( n==16 ){ /* YYYY-MM-DD HH:MM */ + memcpy(zUp, zDate, 16); + memcpy(zUp+16, ":59.999z", 8); + if( !addZ ) zUp[23] = 0; + return zUp; + } + if( n==10 ){ /* YYYY-MM-DD */ + memcpy(zUp, zDate, 10); + memcpy(zUp+10, " 23:59:59.999z", 14); + if( !addZ ) zUp[23] = 0; + return zUp; + } + return zDate; +} + + +/* +** Return the RID that is the "root" of the branch that contains +** check-in "rid". Details depending on eType: +** +** eType==0 The check-in of the parent branch off of which +** the branch containing RID originally diverged. +** +** eType==1 The first check-in of the branch that contains RID. +** +** eType==2 The youngest ancestor of RID that is on the branch +** from which the branch containing RID diverged. +*/ +int start_of_branch(int rid, int eType){ + Stmt q; + int rc; + int ans = rid; + char *zBr = branch_of_rid(rid); + db_prepare(&q, + "WITH RECURSIVE" + " par(pid, ex, cnt) as (" + " SELECT pid, EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagtype>0" + " AND value=%Q AND rid=plink.pid), 1" + " FROM plink WHERE cid=%d AND isprim" + " UNION ALL " + " SELECT plink.pid, EXISTS(SELECT 1 FROM tagxref " + " WHERE tagid=%d AND tagtype>0" + " AND value=%Q AND rid=plink.pid)," + " 1+par.cnt" + " FROM plink, par" + " WHERE cid=par.pid AND isprim AND par.ex " + " LIMIT 100000 " + " )" + " SELECT pid FROM par WHERE ex>=%d ORDER BY cnt DESC LIMIT 1", + TAG_BRANCH, zBr, ans, TAG_BRANCH, zBr, eType%2 + ); + fossil_free(zBr); + rc = db_step(&q); + if( rc==SQLITE_ROW ){ + ans = db_column_int(&q, 0); + } + db_finalize(&q); + if( eType==2 && ans>0 ){ + zBr = branch_of_rid(ans); + ans = compute_youngest_ancestor_in_branch(rid, zBr); + fossil_free(zBr); + } + return ans; +} + +/* +** Find the RID of the most recent object with symbolic tag zTag +** and having a type that matches zType. +** +** Return 0 if there are no matches. +** +** This is a tricky query to do efficiently. +** If the tag is very common (ex: "trunk") then +** we want to use the query identified below as Q1 - which searches +** the most recent EVENT table entries for the most recent with the tag. +** But if the tag is relatively scarce (anything other than "trunk", basically) +** then we want to do the indexed search shown below as Q2. +*/ +static int most_recent_event_with_tag(const char *zTag, const char *zType){ + return db_int(0, + "SELECT objid FROM (" + /* Q1: Begin by looking for the tag in the 30 most recent events */ + "SELECT objid" + " FROM (SELECT * FROM event ORDER BY mtime DESC LIMIT 30) AS ex" + " WHERE type GLOB '%q'" + " AND EXISTS(SELECT 1 FROM tagxref, tag" + " WHERE tag.tagname='sym-%q'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND tagxref.rid=ex.objid)" + " ORDER BY mtime DESC LIMIT 1" + ") UNION ALL SELECT * FROM (" + /* Q2: If the tag is not found in the 30 most recent events, then using + ** the tagxref table to index for the tag */ + "SELECT event.objid" + " FROM tag, tagxref, event" + " WHERE tag.tagname='sym-%q'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND event.objid=tagxref.rid" + " AND event.type GLOB '%q'" + " ORDER BY event.mtime DESC LIMIT 1" + ") LIMIT 1;", + zType, zTag, zTag, zType + ); +} + +/* +** Find the RID for a check-in that is the most recent check-in with +** tag zTag that occurs on or prior to rDate. +** +** See also the performance note on most_recent_event_with_tag() which +** applies to this routine too. +*/ +int last_checkin_with_tag_before_date(const char *zTag, double rLimit){ + Stmt s; + int rid = 0; + if( strncmp(zTag, "tag:", 4)==0 ) zTag += 4; + db_prepare(&s, + "SELECT objid FROM (" + /* Q1: Begin by looking for the tag in the 30 most recent events */ + "SELECT objid" + " FROM (SELECT * FROM event WHERE mtime<=:datelimit" + " ORDER BY mtime DESC LIMIT 30) AS ex" + " WHERE type='ci'" + " AND EXISTS(SELECT 1 FROM tagxref, tag" + " WHERE tag.tagname='sym-%q'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND tagxref.rid=ex.objid)" + " ORDER BY mtime DESC LIMIT 1" + ") UNION ALL SELECT * FROM (" + /* Q2: If the tag is not found in the 30 most recent events, then using + ** the tagxref table to index for the tag */ + "SELECT event.objid" + " FROM tag, tagxref, event" + " WHERE tag.tagname='sym-%q'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND event.objid=tagxref.rid" + " AND event.type='ci'" + " AND event.mtime<=:datelimit" + " ORDER BY event.mtime DESC LIMIT 1" + ") LIMIT 1;", + zTag, zTag + ); + db_bind_double(&s, ":datelimit", rLimit); + if( db_step(&s)==SQLITE_ROW ){ + rid = db_column_int(&s,0); + } + db_finalize(&s); + return rid; +} + +/* +** Find the RID of the first check-in (chronologically) after rStart that +** has tag zTag. +** +** See also the performance note on most_recent_event_with_tag() which +** applies to this routine too. +*/ +int first_checkin_with_tag_after_date(const char *zTag, double rStart){ + Stmt s; + int rid = 0; + if( strncmp(zTag, "tag:", 4)==0 ) zTag += 4; + db_prepare(&s, + "SELECT objid FROM (" + /* Q1: Begin by looking for the tag in the 30 most recent events */ + "SELECT objid" + " FROM (SELECT * FROM event WHERE mtime>=:startdate" + " ORDER BY mtime LIMIT 30) AS ex" + " WHERE type='ci'" + " AND EXISTS(SELECT 1 FROM tagxref, tag" + " WHERE tag.tagname='sym-%q'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND tagxref.rid=ex.objid)" + " ORDER BY mtime LIMIT 1" + ") UNION ALL SELECT * FROM (" + /* Q2: If the tag is not found in the 30 most recent events, then using + ** the tagxref table to index for the tag */ + "SELECT event.objid" + " FROM tag, tagxref, event" + " WHERE tag.tagname='sym-%q'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype>0" + " AND event.objid=tagxref.rid" + " AND event.type='ci'" + " AND event.mtime>=:startdate" + " ORDER BY event.mtime LIMIT 1" + ") LIMIT 1;", + zTag, zTag + ); + db_bind_double(&s, ":startdate", rStart); + if( db_step(&s)==SQLITE_ROW ){ + rid = db_column_int(&s,0); + } + db_finalize(&s); + return rid; +} + +/* +** Return true if character "c" is a character that might have been +** accidentally appended to the end of a URL. +*/ +static int is_trailing_punct(char c){ + return c=='.' || c=='_' || c==')' || c=='>' || c=='!' || c=='?' || c==','; +} + + +/* +** Convert a symbolic name into a RID. Acceptable forms: +** +** * artifact hash (optionally enclosed in [...]) +** * 4-character or larger prefix of an artifact +** * Symbolic Name +** * "tag:" + symbolic name +** * Date or date-time +** * "date:" + Date or date-time +** * symbolic-name ":" date-time +** * "tip" +** +** The following additional forms are available in local checkouts: +** +** * "current" +** * "prev" or "previous" +** * "next" +** +** The following modifier prefixes may be applied to the above forms: +** +** * "root:BR" = The origin of the branch named BR. +** * "start:BR" = The first check-in of the branch named BR. +** * "merge-in:BR" = The most recent merge-in for the branch named BR. +** +** In those forms, BR may be any symbolic form but is assumed to be a +** check-in. Thus root:2021-02-01 would resolve to a check-in, possibly +** in a branch and possibly in the trunk, but never a wiki edit or +** forum post. +** +** Return the RID of the matching artifact. Or return 0 if the name does not +** match any known object. Or return -1 if the name is ambiguous. +** +** The zType parameter specifies the type of artifact: ci, t, w, e, g, f. +** If zType is NULL or "" or "*" then any type of artifact will serve. +** If zType is "br" then find the first check-in of the named branch +** rather than the last. +** +** zType is "ci" in most use cases since we are usually searching for +** a check-in. A value of "ci+" works like "ci" but adds these +** semantics: if zTag is "ckout" and a checkout is open, "ci+" causes +** RID_CKOUT to be returned, in which case g.localOpen will hold the +** RID of the checkout. Conversely, passing in the hash, or another +** symbolic name of the local checkout version, will always result in +** its RID being returned. +** +** Note that the input zTag for types "t" and "e" is the artifact hash of +** the ticket-change or technote-change artifact, not the randomly generated +** hexadecimal identifier assigned to tickets and events. Those identifiers +** live in a separate namespace. +*/ +int symbolic_name_to_rid(const char *zTag, const char *zType){ + int rid = 0; + int ridCkout = 0; + int nTag; + int i; + int startOfBranch = 0; + const char *zXTag; /* zTag with optional [...] removed */ + int nXTag; /* Size of zXTag */ + const char *zDate; /* Expanded date-time string */ + int isCheckin = 0; /* zType==ci = 1, zType==ci+ = 2 */ + + if( zType==0 || zType[0]==0 ){ + zType = "*"; + }else if( zType[0]=='b' ){ + zType = "ci"; + startOfBranch = 1; + } + if( zTag==0 || zTag[0]==0 ) return 0; + else if( 'c'==zType[0] ){ + if( fossil_strcmp(zType,"ci")==0 ){ + isCheckin = 1; + }else if( fossil_strcmp(zType,"ci+")==0 ){ + isCheckin = 2; + zType = "ci"; + } + } + + /* special keyword: "tip" */ + if( fossil_strcmp(zTag, "tip")==0 && (zType[0]=='*' || isCheckin!=0) ){ + rid = db_int(0, + "SELECT objid" + " FROM event" + " WHERE type='ci'" + " ORDER BY event.mtime DESC" + ); + if( rid ) return rid; + } + + if( g.localOpen ) { + ridCkout = db_lget_int("checkout",0); + } + + /* special keywords: "prev", "previous", "current", "ckout", and + ** "next" */ + if( (zType[0]=='*' || isCheckin!=0) && 01 && fossil_strcmp(zTag, "ckout")==0 ){ + rid = RID_CKOUT; + assert(ridCkout>0); + g.localOpen = ridCkout; + } + if( rid ) return rid; + } + + /* Date and times */ + if( memcmp(zTag, "date:", 5)==0 ){ + zDate = fossil_expand_datetime(&zTag[5],0,1); + if( zDate==0 ) zDate = &zTag[5]; + rid = db_int(0, + "SELECT objid FROM event" + " WHERE mtime<=julianday(%Q,fromLocal()) AND type GLOB '%q'" + " ORDER BY mtime DESC LIMIT 1", + fossil_roundup_date(zDate), zType); + return rid; + } + if( fossil_isdate(zTag) ){ + rid = db_int(0, + "SELECT objid FROM event" + " WHERE mtime<=julianday(%Q,fromLocal()) AND type GLOB '%q'" + " ORDER BY mtime DESC LIMIT 1", + fossil_roundup_date(zTag), zType); + if( rid) return rid; + } + + /* Deprecated date & time formats: "local:" + date-time and + ** "utc:" + date-time */ + if( memcmp(zTag, "local:", 6)==0 ){ + rid = db_int(0, + "SELECT objid FROM event" + " WHERE mtime<=julianday(%Q) AND type GLOB '%q'" + " ORDER BY mtime DESC LIMIT 1", + &zTag[6], zType); + return rid; + } + if( memcmp(zTag, "utc:", 4)==0 ){ + rid = db_int(0, + "SELECT objid FROM event" + " WHERE mtime<=julianday('%qz') AND type GLOB '%q'" + " ORDER BY mtime DESC LIMIT 1", + fossil_roundup_date(&zTag[4]), zType); + return rid; + } + + /* "tag:" + symbolic-name */ + if( memcmp(zTag, "tag:", 4)==0 ){ + rid = most_recent_event_with_tag(&zTag[4], zType); + if( startOfBranch ) rid = start_of_branch(rid,1); + return rid; + } + + /* root:BR -> The origin of the branch named BR */ + if( strncmp(zTag, "root:", 5)==0 ){ + rid = symbolic_name_to_rid(zTag+5, zType); + return start_of_branch(rid, 0); + } + + /* start:BR -> The first check-in on branch named BR */ + if( strncmp(zTag, "start:", 6)==0 ){ + rid = symbolic_name_to_rid(zTag+6, zType); + return start_of_branch(rid, 1); + } + + /* merge-in:BR -> Most recent merge-in for the branch named BR */ + if( strncmp(zTag, "merge-in:", 9)==0 ){ + rid = symbolic_name_to_rid(zTag+9, zType); + return start_of_branch(rid, 2); + } + + /* symbolic-name ":" date-time */ + nTag = strlen(zTag); + for(i=0; i0 " + " AND event.objid=tagxref.rid " + " AND event.mtime<=julianday(%Q,fromLocal())" + " AND event.type GLOB '%q'", + zTagBase, fossil_roundup_date(zXDate), zType + ); + fossil_free(zDate); + fossil_free(zTagBase); + return rid; + } + + /* Remove optional [...] */ + zXTag = zTag; + nXTag = nTag; + if( zXTag[0]=='[' ){ + zXTag++; + nXTag--; + } + if( nXTag>0 && zXTag[nXTag-1]==']' ){ + nXTag--; + } + + /* artifact hash or prefix */ + if( nXTag>=4 && nXTag<=HNAME_MAX && validate16(zXTag, nXTag) ){ + Stmt q; + char zUuid[HNAME_MAX+1]; + memcpy(zUuid, zXTag, nXTag); + zUuid[nXTag] = 0; + canonical16(zUuid, nXTag); + rid = 0; + if( zType[0]=='*' ){ + db_prepare(&q, "SELECT rid FROM blob WHERE uuid GLOB '%q*'", zUuid); + }else{ + db_prepare(&q, + "SELECT blob.rid" + " FROM blob CROSS JOIN event" + " WHERE blob.uuid GLOB '%q*'" + " AND event.objid=blob.rid" + " AND event.type GLOB '%q'", + zUuid, zType + ); + } + if( db_step(&q)==SQLITE_ROW ){ + rid = db_column_int(&q, 0); + if( db_step(&q)==SQLITE_ROW ) rid = -1; + } + db_finalize(&q); + if( rid ) return rid; + } + + if( zType[0]=='w' ){ + rid = db_int(0, + "SELECT event.objid, max(event.mtime)" + " FROM tag, tagxref, event" + " WHERE tag.tagname='wiki-%q' " + " AND tagxref.tagid=tag.tagid AND tagxref.tagtype>0 " + " AND event.objid=tagxref.rid " + " AND event.type GLOB '%q'", + zTag, zType + ); + }else{ + rid = most_recent_event_with_tag(zTag, zType); + } + + if( rid>0 ){ + if( startOfBranch ) rid = start_of_branch(rid,1); + return rid; + } + + /* Pure numeric date/time */ + zDate = fossil_expand_datetime(zTag, 0,1); + if( zDate ){ + rid = db_int(0, + "SELECT objid FROM event" + " WHERE mtime<=julianday(%Q,fromLocal()) AND type GLOB '%q'" + " ORDER BY mtime DESC LIMIT 1", + fossil_roundup_date(zDate), zType); + if( rid) return rid; + } + + + /* Undocumented: numeric tags get translated directly into the RID */ + if( memcmp(zTag, "rid:", 4)==0 ){ + zTag += 4; + for(i=0; fossil_isdigit(zTag[i]); i++){} + if( zTag[i]==0 ){ + if( strcmp(zType,"*")==0 ){ + rid = atoi(zTag); + }else{ + rid = db_int(0, + "SELECT event.objid" + " FROM event" + " WHERE event.objid=%s" + " AND event.type GLOB '%q'", zTag /*safe-for-%s*/, zType); + } + } + return rid; + } + + /* If nothing matches and the name ends with punctuation, + ** then the name might have originated from a URL in plain text + ** that was incorrectly extracted from the text. Try to remove + ** the extra punctuation and rerun the match. + */ + if( nTag>4 + && is_trailing_punct(zTag[nTag-1]) + && !is_trailing_punct(zTag[nTag-2]) + ){ + char *zNew = fossil_strndup(zTag, nTag-1); + rid = symbolic_name_to_rid(zNew,zType); + fossil_free(zNew); + }else + if( nTag>5 + && is_trailing_punct(zTag[nTag-1]) + && is_trailing_punct(zTag[nTag-2]) + && !is_trailing_punct(zTag[nTag-3]) + ){ + char *zNew = fossil_strndup(zTag, nTag-2); + rid = symbolic_name_to_rid(zNew,zType); + fossil_free(zNew); + } + return rid; +} + +/* +** Convert a symbolic name used as an argument to the a=, b=, or c= +** query parameters of timeline into a julianday mtime value. +** +** If pzDisplay is not null, then display text for the symbolic name might +** be written into *pzDisplay. But that is not guaranteed. +** +** If bRoundUp is true and the symbolic name is a timestamp with less +** than millisecond resolution, then the timestamp is rounding up to the +** largest millisecond consistent with that timestamp. If bRoundUp is +** false, then the resulting time is obtained by extending the timestamp +** with zeros (hence rounding down). Use bRoundUp==1 if the result +** will be used in mtime<=$RESULT and use bRoundUp==0 if the result +** will be used in mtime>=$RESULT. +*/ +double symbolic_name_to_mtime( + const char *z, /* Input symbolic name */ + const char **pzDisplay, /* Perhaps write display text here, if not NULL */ + int bRoundUp /* Round up if true */ +){ + 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, bRoundUp); + if( zDate!=0 ){ + mtime = db_double(0.0, "SELECT julianday(%Q,fromLocal())", + bRoundUp ? fossil_roundup_date(zDate) : zDate); + if( mtime>0.0 ){ + if( pzDisplay ){ + zDate = fossil_expand_datetime(z,0,0); + *pzDisplay = fossil_strdup(zDate); + } + return mtime; + } + } + rid = symbolic_name_to_rid(z, "*"); + if( rid ){ + mtime = mtime_of_rid(rid, 0.0); + }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; +} + +/* +** This routine takes a user-entered string and tries to convert it to +** an artifact hash. +** +** We first try to treat the string as an artifact hash, or at least a +** unique prefix of an artifact hash. The input may be in mixed case. +** If we are passed such a string, this routine has the effect of +** converting the hash [prefix] to canonical form. +** +** If the input is not a hash or a hash prefix, then try to resolve +** the name as a tag. If multiple tags match, pick the latest. +** A caller can force this routine to skip the hash case above by +** prefixing the string with "tag:", a useful property when the tag +** may be misinterpreted as a hex ASCII string. (e.g. "decade" or "facade") +** +** If the input is not a tag, then try to match it as an ISO-8601 date +** string YYYY-MM-DD HH:MM:SS and pick the nearest check-in to that date. +** If the input is of the form "date:*" then always resolve the name as +** a date. The forms "utc:*" and "local:" are deprecated. +** +** Return 0 on success. Return 1 if the name cannot be resolved. +** Return 2 name is ambiguous. +*/ +int name_to_uuid(Blob *pName, int iErrPriority, const char *zType){ + char *zName = blob_str(pName); + int rid = symbolic_name_to_rid(zName, zType); + if( rid<0 ){ + fossil_error(iErrPriority, "ambiguous name: %s", zName); + return 2; + }else if( rid==0 ){ + fossil_error(iErrPriority, "cannot resolve name: %s", zName); + return 1; + }else{ + blob_reset(pName); + if( RID_CKOUT==rid ) { + rid = g.localOpen; + } + db_blob(pName, "SELECT uuid FROM blob WHERE rid=%d", rid); + return 0; + } +} + +/* +** This routine is similar to name_to_uuid() except in the form it +** takes its parameters and returns its value, and in that it does not +** treat errors as fatal. zName must be an artifact hash or prefix of +** a hash. zType is also as described for name_to_uuid(). If +** zName does not resolve, 0 is returned. If it is ambiguous, a +** negative value is returned. On success the rid is returned and +** pUuid (if it is not NULL) is set to a newly-allocated string, +** the full hash, which must eventually be free()d by the caller. +*/ +int name_to_uuid2(const char *zName, const char *zType, char **pUuid){ + int rid = symbolic_name_to_rid(zName, zType); + if((rid>0) && pUuid){ + *pUuid = (rid=4 && nLen<=HNAME_MAX && validate16(zName, nLen) ){ + c = db_int(0, + "SELECT" + " (SELECT count(*) FROM ticket" + " WHERE tkt_uuid GLOB '%q*') +" + " (SELECT count(*) FROM tag" + " WHERE tagname GLOB 'event-%q*') +" + " (SELECT count(*) FROM blob" + " WHERE uuid GLOB '%q*');", + zName, zName, zName + ); + if( c<2 ) c = 0; + } + return c; +} + +/* +** COMMAND: test-name-to-id +** +** Usage: %fossil test-name-to-id [--count N] [--type ARTIFACT_TYPE] NAME +** +** Convert a NAME to a full artifact ID. Repeat the conversion N +** times (for timing purposes) if the --count option is given. +*/ +void test_name_to_id(void){ + int i; + int n = 0; + Blob name; + const char *zType; + + db_must_be_within_tree(); + if( (zType = find_option("type","t",1))==0 ){ + zType = "*"; + } + for(i=2; i ", g.argv[i]); + if( name_to_uuid(&name, 1, zType) ){ + fossil_print("ERROR: %s\n", g.zErrMsg); + fossil_error_reset(); + }else{ + fossil_print("%s\n", blob_buffer(&name)); + } + blob_reset(&name); + }while( n-- > 0 ); + } +} + +/* +** Convert a name to a rid. If the name can be any of the various forms +** accepted: +** +** * artifact hash or prefix thereof +** * symbolic name +** * date +** * label:date +** * prev, previous +** * next +** * tip +** +** This routine is used by command-line routines to resolve command-line inputs +** into a rid. +*/ +int name_to_typed_rid(const char *zName, const char *zType){ + int rid; + + if( zName==0 || zName[0]==0 ) return 0; + rid = symbolic_name_to_rid(zName, zType); + if( rid<0 ){ + fossil_fatal("ambiguous name: %s", zName); + }else if( rid==0 ){ + fossil_fatal("cannot resolve name: %s", zName); + } + return rid; +} +int name_to_rid(const char *zName){ + return name_to_typed_rid(zName, "*"); +} + +/* +** Try to resolve zQP1 into a check-in name. If zQP1 does not exist, +** return 0. If zQP1 exists but cannot be resolved, then also try to +** resolve zQP2 if it exists. If zQP1 cannot be resolved but zQP2 does +** not exist, then raise an error. If both zQP1 and zQP2 exists but +** neither can be resolved, also raise an error. +** +** If pzPick is not a NULL pointer, then *pzPick to be the value of whichever +** query parameter ended up being used. +*/ +int name_choice(const char *zQP1, const char *zQP2, const char **pzPick){ + const char *zName, *zName2; + int rid; + zName = P(zQP1); + if( zName==0 || zName[0]==0 ) return 0; + rid = symbolic_name_to_rid(zName, "ci"); + if( rid>0 ){ + if( pzPick ) *pzPick = zName; + return rid; + } + if( rid<0 ){ + fossil_fatal("ambiguous name: %s", zName); + } + zName2 = P(zQP2); + if( zName2==0 || zName2[0]==0 ){ + fossil_fatal("cannot resolve name: %s", zName); + } + if( pzPick ) *pzPick = zName2; + return name_to_typed_rid(zName2, "ci"); +} + +/* +** WEBPAGE: ambiguous +** URL: /ambiguous?name=NAME&src=WEBPAGE +** +** The NAME given by the name parameter is ambiguous. Display a page +** that shows all possible choices and let the user select between them. +** +** The src= query parameter is optional. If omitted it defaults +** to "info". +*/ +void ambiguous_page(void){ + Stmt q; + const char *zName = P("name"); + const char *zSrc = PD("src","info"); + char *z; + + if( zName==0 || zName[0]==0 || zSrc==0 || zSrc[0]==0 ){ + fossil_redirect_home(); + } + style_header("Ambiguous Artifact ID"); + @

The artifact hash prefix %h(zName) is ambiguous and might + @ mean any of the following: + @

    + z = fossil_strdup(zName); + canonical16(z, strlen(z)); + db_prepare(&q, "SELECT uuid, rid FROM blob WHERE uuid GLOB '%q*'", z); + while( db_step(&q)==SQLITE_ROW ){ + const char *zUuid = db_column_text(&q, 0); + int rid = db_column_int(&q, 1); + @
  1. + @ %s(zUuid) - + object_description(rid, 0, 0, 0); + @

  2. + } + db_finalize(&q); + db_prepare(&q, + " SELECT tkt_rid, tkt_uuid, title" + " FROM ticket, ticketchng" + " WHERE ticket.tkt_id = ticketchng.tkt_id" + " AND tkt_uuid GLOB '%q*'" + " GROUP BY tkt_uuid" + " ORDER BY tkt_ctime DESC", z); + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q, 0); + const char *zUuid = db_column_text(&q, 1); + const char *zTitle = db_column_text(&q, 2); + @
  3. + @ %s(zUuid) - + @

      + @ Ticket + hyperlink_to_version(zUuid); + @ - %h(zTitle). + @
      • + object_description(rid, 0, 0, 0); + @
      + @

    • + } + db_finalize(&q); + db_prepare(&q, + "SELECT rid, uuid FROM" + " (SELECT tagxref.rid AS rid, substr(tagname, 7) AS uuid" + " FROM tagxref, tag WHERE tagxref.tagid = tag.tagid" + " AND tagname GLOB 'event-%q*') GROUP BY uuid", z); + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q, 0); + const char* zUuid = db_column_text(&q, 1); + @
    • + @ %s(zUuid) - + @

      • + object_description(rid, 0, 0, 0); + @
      + @

    • + } + @
    + db_finalize(&q); + style_finish_page(); +} + +/* +** Convert the name in CGI parameter zParamName into a rid and return that +** rid. If the CGI parameter is missing or is not a valid artifact tag, +** return 0. If the CGI parameter is ambiguous, redirect to a page that +** shows all possibilities and do not return. +*/ +int name_to_rid_www(const char *zParamName){ + int rid; + const char *zName = P(zParamName); +#ifdef FOSSIL_ENABLE_JSON + if(!zName && fossil_has_json()){ + zName = json_find_option_cstr(zParamName,NULL,NULL); + } +#endif + if( zName==0 || zName[0]==0 ) return 0; + rid = symbolic_name_to_rid(zName, "*"); + if( rid<0 ){ + cgi_redirectf("%R/ambiguous/%T?src=%t", zName, g.zPath); + rid = 0; + } + return rid; +} + +/* +** Given an RID of a structural artifact, which is assumed to be +** valid, this function returns one of the CFTYPE_xxx values +** describing the record type, or 0 if the RID does not refer to an +** artifact record (as determined by reading the event table). +** +** Note that this function never returns CFTYPE_ATTACHMENT or +** CFTYPE_CLUSTER because those are not used in the event table. Thus +** it cannot be used to distinguish those artifact types from +** non-artifact file content. +** +** Potential TODO: if the rid is not found in the timeline, try to +** match it to a client file and return a hypothetical new +** CFTYPE_OPAQUE or CFTYPE_NONARTIFACT if a match is found. +*/ +int whatis_rid_type(int rid){ + Stmt q = empty_Stmt; + int type = 0; + /* Check for entries on the timeline that reference this object */ + db_prepare(&q, + "SELECT type FROM event WHERE objid=%d", rid); + if( db_step(&q)==SQLITE_ROW ){ + switch( db_column_text(&q,0)[0] ){ + case 'c': type = CFTYPE_MANIFEST; break; + case 'w': type = CFTYPE_WIKI; break; + case 'e': type = CFTYPE_EVENT; break; + case 'f': type = CFTYPE_FORUM; break; + case 't': type = CFTYPE_TICKET; break; + case 'g': type = CFTYPE_CONTROL; break; + default: break; + } + } + db_finalize(&q); + return type; +} + +/* +** A proxy for whatis_rid_type() which returns a brief string (in +** static memory) describing the record type. Returns NULL if rid does +** not refer to an artifact record (as determined by reading the event +** table). The returned string is intended to be used in headers which +** can refer to different artifact types. It is not "definitive," in +** that it does not distinguish between closely-related types like +** wiki creation, edit, and removal. +*/ +char const * whatis_rid_type_label(int rid){ + char const * zType = 0; + switch( whatis_rid_type(rid) ){ + case CFTYPE_MANIFEST: zType = "Check-in"; break; + case CFTYPE_WIKI: zType = "Wiki-edit"; break; + case CFTYPE_EVENT: zType = "Technote"; break; + case CFTYPE_FORUM: zType = "Forum-post"; break; + case CFTYPE_TICKET: zType = "Ticket-change"; break; + case CFTYPE_CONTROL: zType = "Tag-change"; break; + default: break; + } + return zType; +} + +/* +** This is the output routine for whatis_rid(). If pOut is NULL +** then write zKey and zValue on fossil_print(). But if pOut is +** not NULL, append the key and value as a term of a JSON object. +*/ +static void whatis_line( + sqlite3_str *pOut, + const char *zKey, + const char *zValue, + ... +){ + char *zArg; + va_list ap; + va_start(ap, zValue); + zArg = sqlite3_vmprintf(zValue,ap); + va_end(ap); + if( pOut ){ + int n = sqlite3_str_length(pOut); + if( n>1 && sqlite3_str_value(pOut)[n-1]!='{' ){ + sqlite3_str_append(pOut, ",", 1); + } + sqlite3_str_appendf(pOut, "%J:%J", zKey, zArg); + }else{ + int n = 11 - (int)strlen(zKey); + if( n<=0 ) n = 0; + fossil_print("%s:%*s%s\n",zKey,n,"",zArg); + } + sqlite3_free(zArg); +} + +/* +** Flag values for whatis_rid(). +*/ +#if INTERFACE +#define WHATIS_VERBOSE 0x01 /* Extra output */ +#define WHATIS_BRIEF 0x02 /* Omit unnecessary output */ +#define WHATIS_REPO 0x04 /* Show repository name */ +#define WHATIS_OMIT_UNK 0x08 /* Do not show "unknown" lines */ +#define WHATIS_HASHONLY 0x10 /* Show only the hash */ +#define WHATIS_JSON 0x20 /* Render as JSON */ +#endif + +/* +** Generate a description of artifact "rid". +** +** Send results to stdout using fossil_print() if pOut==0 or if +** WHATIS_HASHONLY is set. But without WHATIS_HASHONLY and if +** pOut!=0, then write a JSON description of the object into pOut. +*/ +void whatis_rid(int rid, int flags, sqlite3_str *pOut){ + Stmt q; + int cnt; + + if( pOut ) sqlite3_str_appendf(pOut, "{\"rid\":%d", rid); + + /* Basic information about the object. */ + db_prepare(&q, + "SELECT uuid, size, datetime(mtime,toLocal()), ipaddr" + " FROM blob, rcvfrom" + " WHERE rid=%d" + " AND rcvfrom.rcvid=blob.rcvid", + rid); + if( db_step(&q)==SQLITE_ROW ){ + if( flags & WHATIS_HASHONLY ){ + fossil_print("%s\n", db_column_text(&q,0)); + }else if( flags & WHATIS_VERBOSE ){ + whatis_line(pOut, "artifact", "%s (%d)", db_column_text(&q,0), rid); + whatis_line(pOut, "size", "%d bytes", db_column_int(&q,1)); + whatis_line(pOut, "received", "%s from %s", + db_column_text(&q, 2), + db_column_text(&q, 3)); + }else{ + whatis_line(pOut, "artifact","%s", db_column_text(&q,0)); + whatis_line(pOut, "size", "%d bytes", db_column_int(&q,1)); + } + }else if( pOut ){ + sqlite3_str_appendf(pOut, ",\"artifact\":null"); + } + db_finalize(&q); + if( flags & WHATIS_HASHONLY ) return; + + /* Report any symbolic tags on this artifact */ + db_prepare(&q, + "SELECT substr(tagname,5)" + " FROM tag JOIN tagxref ON tag.tagid=tagxref.tagid" + " WHERE tagxref.rid=%d" + " AND tagxref.tagtype<>0" + " AND tagname GLOB 'sym-*'" + " ORDER BY 1", + rid + ); + cnt = 0; + while( db_step(&q)==SQLITE_ROW ){ + const char *zTag = (const char*)db_column_text(&q,0); + if( pOut ){ + const char *zSep = cnt==0 ? ",tags:[" : ","; + sqlite3_str_appendf(pOut, "%s%J", zSep, zTag); + }else{ + const char *zPrefix = cnt ? ", " : "tags: "; + fossil_print("%s%s", zPrefix, db_column_text(&q,0)); + } + cnt++; + } + if( cnt ){ + if( pOut ){ + sqlite3_str_append(pOut, "]", 1); + }else{ + fossil_print("\n"); + } + } + db_finalize(&q); + + /* Report any HIDDEN, PRIVATE, CLUSTER, or CLOSED tags on this artifact */ + db_prepare(&q, + "SELECT tagname" + " FROM tag JOIN tagxref ON tag.tagid=tagxref.tagid" + " WHERE tagxref.rid=%d" + " AND tag.tagid IN (5,6,7,9)" + " ORDER BY 1", + rid + ); + cnt = 0; + while( db_step(&q)==SQLITE_ROW ){ + const char *zTag = (const char*)db_column_text(&q,0); + if( pOut ){ + const char *zSep = cnt==0 ? ",\"raw-tags\":[" : ","; + sqlite3_str_appendf(pOut, "%s%J", zSep, zTag); + }else{ + const char *zPrefix = cnt ? ", " : "raw-tags: "; + fossil_print("%s%s", zPrefix, db_column_text(&q,0)); + } + cnt++; + } + if( cnt ){ + if( pOut ){ + sqlite3_str_append(pOut, "]", 1); + }else{ + fossil_print("\n"); + } + } + db_finalize(&q); + + /* Check for entries on the timeline that reference this object */ + db_prepare(&q, + "SELECT" + " type," + " datetime(mtime,toLocal())," + " coalesce(euser,user)," + " coalesce(ecomment,comment)," + " if(type='t',(SELECT substr(tagname,5) FROM tag" + " WHERE tag.tagid=event.tagid))" + "FROM event WHERE objid=%d", + rid); + if( db_step(&q)==SQLITE_ROW ){ + const char *zType; + char eType = db_column_text(&q,0)[0]; + switch( eType ){ + case 'c': zType = "Check-in"; break; + case 'w': zType = "Wiki-edit"; break; + case 'e': zType = "Technote"; break; + case 'f': zType = "Forum-post"; break; + case 't': zType = "Ticket-change"; break; + case 'g': zType = "Tag-change"; break; + default: zType = "Unknown"; break; + } + whatis_line(pOut, "type", "%s by %s on %s", zType, db_column_text(&q,2), + db_column_text(&q, 1)); + if( eType=='t' && db_column_type(&q,4)==SQLITE_TEXT ){ + whatis_line(pOut, "ticket-id", "%s", db_column_text(&q,4)); + } + if( pOut ){ + sqlite3_str_appendf(pOut, ",\"comment\":%J", db_column_text(&q,3)); + }else{ + fossil_print("comment: "); + comment_print(db_column_text(&q,3), 0, 12, -1, get_comment_format()); + } + cnt++; + } + db_finalize(&q); + + /* Check to see if this object is used as a file in a check-in */ + db_prepare(&q, + "SELECT filename.name, blob.uuid, datetime(event.mtime,toLocal())," + " coalesce(euser,user), coalesce(ecomment,comment)," + " coalesce((SELECT value FROM tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=mlink.mid),'trunk')" + " FROM mlink, filename, blob, event" + " WHERE mlink.fid=%d" + " AND filename.fnid=mlink.fnid" + " AND event.objid=mlink.mid" + " AND blob.rid=mlink.mid" + " ORDER BY event.mtime %s /*sort*/", + TAG_BRANCH, rid, + (flags & WHATIS_BRIEF) ? "LIMIT 1" : "DESC"); + while( db_step(&q)==SQLITE_ROW ){ + if( flags & WHATIS_BRIEF ){ + whatis_line(pOut, "mtime","%s", db_column_text(&q,2)); + } + if( pOut ){ + whatis_line(pOut,"file","%s", db_column_text(&q,0)); + whatis_line(pOut,"part-of","%s", db_column_text(&q,1)); + }else{ + fossil_print("file: %s\n", db_column_text(&q,0)); + fossil_print(" part of [%S] on branch %s by %s on %s\n", + db_column_text(&q, 1), + db_column_text(&q, 5), + db_column_text(&q, 3), + db_column_text(&q, 2)); + fossil_print(" "); + comment_print(db_column_text(&q,4), 0, 12, -1, get_comment_format()); + } + cnt++; + } + db_finalize(&q); + + /* Check to see if this object is used as an attachment */ + db_prepare(&q, + "SELECT attachment.filename," + " attachment.comment," + " attachment.user," + " datetime(attachment.mtime,toLocal())," + " attachment.target," + " CASE WHEN EXISTS(SELECT 1 FROM tag WHERE tagname=('tkt-'||target))" + " THEN 'ticket'" + " WHEN EXISTS(SELECT 1 FROM tag WHERE tagname=('wiki-'||target))" + " THEN 'wiki' END," + " attachment.attachid," + " (SELECT uuid FROM blob WHERE rid=attachid)" + " FROM attachment JOIN blob ON attachment.src=blob.uuid" + " WHERE blob.rid=%d", + rid + ); + while( db_step(&q)==SQLITE_ROW ){ + if( pOut ){ + whatis_line(pOut, "attachment", "%s", db_column_text(&q,0)); + whatis_line(pOut, "attached-to", "%s %s", + db_column_text(&q,5), db_column_text(&q,4)); + }else{ + fossil_print("attachment: %s\n", db_column_text(&q,0)); + fossil_print(" attached to %s %s\n", + db_column_text(&q,5), db_column_text(&q,4)); + if( flags & WHATIS_VERBOSE ){ + fossil_print(" via %s (%d)\n", + db_column_text(&q,7), db_column_int(&q,6)); + }else{ + fossil_print(" via %s\n", + db_column_text(&q,7)); + } + fossil_print(" by user %s on %s\n", + db_column_text(&q,2), db_column_text(&q,3)); + fossil_print(" "); + comment_print(db_column_text(&q,1), 0, 12, -1, get_comment_format()); + } + cnt++; + } + db_finalize(&q); + + /* If other information available, try to describe the object */ + if( cnt==0 && pOut==0 ){ + char *zWhere = mprintf("=%d", rid); + char *zDesc; + describe_artifacts(zWhere); + free(zWhere); + zDesc = db_text(0, + "SELECT printf('%%-12s%%s %%s',type||':',summary,substr(ref,1,16))" + " FROM description WHERE rid=%d", rid); + fossil_print("%s\n", zDesc); + fossil_free(zDesc); + } + + if( pOut ) sqlite3_str_append(pOut, "}", 1); +} + +/* +** Generate a description of artifact from it symbolic name. +*/ +void whatis_artifact( + const char *zName, /* Symbolic name or full hash */ + const char *zFileName,/* Optional: original filename (in file mode) */ + const char *zType, /* Artifact type filter */ + int mFlags, /* WHATIS_* flags */ + sqlite3_str *pOut /* Write JSON here, if not NULL */ +){ + int rid = symbolic_name_to_rid(zName, zType); + size_t nName; + char *zC = 0; + int nTkt = 0; + + if( pOut ){ + mFlags &= ~(WHATIS_HASHONLY|WHATIS_REPO); + zFileName = 0; + } + if( (nName = strlen(zName))>=4 && validate16(zName,nName) ){ + zC = fossil_strdup(zName); + canonical16(zC, nName); + nTkt = db_int(0,"SELECT count(*) FROM tag WHERE tagname GLOB 'tkt-%q*'",zC); + if( nTkt>0 ){ + if( rid==0 ){ + rid = db_int(0, + "SELECT srcid FROM tag, tagxref" + " WHERE tag.tagname GLOB 'tkt-%q*'" + " AND tagxref.tagid=tag.tagid" + " AND tagxref.tagtype=1" + " ORDER BY tagxref.mtime", + zC + ); + }else{ + rid = -1; + } + } + } + if( rid<0 ){ + Stmt q; + int cnt = 0; + if( mFlags & WHATIS_REPO ){ + fossil_print("\nrepository: %s\n", g.zRepositoryName); + } + if( zFileName ){ + fossil_print("%-12s%s\n", "name:", zFileName); + } + if( pOut ){ + sqlite3_str_appendall(pOut, "{\"ambiguous\":["); + }else{ + fossil_print("%-12s%s (ambiguous)\n", "hash:", zName); + } + db_prepare(&q, + "SELECT rid FROM blob WHERE uuid>=lower(%Q) AND uuid<(lower(%Q)||'z')", + zName, zName + ); + while( db_step(&q)==SQLITE_ROW ){ + if( cnt++ ){ + if( pOut ){ + sqlite3_str_append(pOut, ",", 1); + }else{ + fossil_print("%12s---- meaning #%d ----\n", " ", cnt); + } + } + whatis_rid(db_column_int(&q, 0), mFlags, pOut); + } + db_finalize(&q); + if( nTkt>0 ){ + db_prepare(&q, + "SELECT (SELECT srcid FROM tagxref" + " WHERE tagxref.tagid=tag.tagid" + " AND tagxref.tagtype=1" + " ORDER BY mtime LIMIT 1)" + " FROM tag WHERE tagname GLOB 'tkt-%q*'", + zC + ); + while( db_step(&q)==SQLITE_ROW ){ + if( pOut ){ + sqlite3_str_append(pOut, ",", 1); + }else{ + fossil_print("%12s---- meaning #%d ----\n", " ", cnt); + } + whatis_rid(db_column_int(&q, 0), mFlags, pOut); + } + db_finalize(&q); + } + if( pOut ) sqlite3_str_append(pOut, "]}", 2); + }else if( rid==0 ){ + if( (mFlags & (WHATIS_OMIT_UNK|WHATIS_HASHONLY))==0 ){ + /* 0123456789 12 */ + if( zFileName ){ + fossil_print("%-12s%s\n", "name:", zFileName); + } + if( pOut ) sqlite3_str_append(pOut, "{", 1); + whatis_line(pOut, "unknown", "%s", zName); + if( pOut ) sqlite3_str_append(pOut, "}", 1); + } + }else{ + if( mFlags & WHATIS_REPO ){ + fossil_print("\nrepository: %s\n", g.zRepositoryName); + } + if( zFileName ){ + zName = zFileName; + } + if( (mFlags & WHATIS_HASHONLY)==0 && pOut==0 ){ + whatis_line(pOut, "name", "%s", zName); + } + whatis_rid(rid, mFlags, pOut); + } + fossil_free(zC); +} + +/* +** COMMAND: whatis* +** +** Usage: %fossil whatis NAME +** +** Resolve the symbol NAME into its canonical artifact hash +** artifact name and provide a description of what role that artifact +** plays. +** +** Options: +** -f|--file Find artifacts with the same hash as file NAME. +** If NAME is "-", read content from standard input. +** -h|--hash Show only the hash of matching artifacts. +** -q|--quiet Show nothing if NAME is not found +** -R REPO_FILE Specifies the repository db to use. Default is +** the current check-out's repository. +** --type TYPE Only find artifacts of TYPE (one of: 'ci', 't', +** 'w', 'g', or 'e') +** -v|--verbose Provide extra information (such as the RID) +*/ +void whatis_cmd(void){ + int mFlags = 0; + int fileFlag; + int i; + const char *zType = 0; + sqlite3_str *pOut = 0; + db_find_and_open_repository(0,0); + if( find_option("verbose","v",0)!=0 ){ + mFlags |= WHATIS_VERBOSE; + } + if( find_option("hash","h",0)!=0 ){ + mFlags |= WHATIS_HASHONLY; + } + if( g.fQuiet ){ + mFlags |= WHATIS_OMIT_UNK | WHATIS_REPO; + } + fileFlag = find_option("file","f",0)!=0; + zType = find_option("type",0,1); + + if( find_option("json",0,0)!=0 ){ /* undocumented test option */ + pOut = sqlite3_str_new(0); + } + + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc<3 ) usage("NAME ..."); + for(i=2; i2 ) fossil_print("%.79c\n",'-'); + if( fileFlag ){ + Blob in; + Blob hash = empty_blob; + const char *zHash; + /* Always follow symlinks (when applicable) */ + blob_read_from_file(&in, zName, ExtFILE); + + /* First check the auxiliary hash to see if there is already an artifact + ** that uses the auxiliary hash name */ + hname_hash(&in, 1, &hash); + zHash = (const char*)blob_str(&hash); + if( fast_uuid_to_rid(zHash)==0 ){ + /* No existing artifact with the auxiliary hash name. Therefore, use + ** the primary hash name. */ + blob_reset(&hash); + hname_hash(&in, 0, &hash); + zHash = (const char*)blob_str(&hash); + } + whatis_artifact(zHash, zName, zType, mFlags, pOut); + blob_reset(&hash); + }else{ + whatis_artifact(zName, 0, zType, mFlags, pOut); + } + if( pOut ){ + fossil_print("%s\n", sqlite3_str_value(pOut)); + sqlite3_str_truncate(pOut, 0); + } + } + if( pOut ) sqlite3_str_free(pOut); +} + +/* +** This is an SQL function that does the rough equivalent of the +** "whatis" command. The argument can be a text identifier, or a +** integer RID. +*/ +void whatis_sql_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3_str *pOut = sqlite3_str_new(0); + int n; + assert( argc==1 ); + if( sqlite3_value_type(argv[0])==SQLITE_INTEGER ){ + int rid = sqlite3_value_int(argv[0]); + whatis_rid(rid, 0, pOut); + }else if( sqlite3_value_type(argv[0])==SQLITE_TEXT ){ + const char *zName = (const char*)sqlite3_value_text(argv[0]); + whatis_artifact(zName, 0, 0, 0, pOut); + }else{ + sqlite3_str_free(pOut); + return; + } + n = sqlite3_str_length(pOut); + sqlite3_result_text64(context, sqlite3_str_finish(pOut), n, + sqlite3_free, SQLITE_UTF8); + sqlite3_result_subtype(context, 'J'); +} + + +/* +** COMMAND: test-whatis-all +** +** Usage: %fossil test-whatis-all +** +** Show "whatis" information about every artifact in the repository +*/ +void test_whatis_all_cmd(void){ + Stmt q; + int cnt = 0; + db_find_and_open_repository(0,0); + db_prepare(&q, "SELECT rid FROM blob ORDER BY rid"); + while( db_step(&q)==SQLITE_ROW ){ + if( cnt++ ) fossil_print("%.79c\n", '-'); + whatis_rid(db_column_int(&q,0), 1, 0); + } + db_finalize(&q); +} + + +/* +** COMMAND: test-ambiguous +** +** Usage: %fossil test-ambiguous [--minsize N] +** +** Show a list of ambiguous artifact hash abbreviations of N characters or +** more where N defaults to 4. Change N to a different value using +** the "--minsize N" command-line option. +*/ +void test_ambiguous_cmd(void){ + Stmt q, ins; + int i; + int minSize = 4; + const char *zMinsize; + char zPrev[100]; + db_find_and_open_repository(0,0); + zMinsize = find_option("minsize",0,1); + if( zMinsize && atoi(zMinsize)>0 ) minSize = atoi(zMinsize); + db_multi_exec("CREATE TEMP TABLE dups(uuid, cnt)"); + db_prepare(&ins,"INSERT INTO dups(uuid) VALUES(substr(:uuid,1,:cnt))"); + db_prepare(&q, + "SELECT uuid FROM blob " + "UNION " + "SELECT substr(tagname,7) FROM tag WHERE tagname GLOB 'event-*' " + "UNION " + "SELECT tkt_uuid FROM ticket " + "ORDER BY 1" + ); + zPrev[0] = 0; + while( db_step(&q)==SQLITE_ROW ){ + const char *zUuid = db_column_text(&q, 0); + for(i=0; zUuid[i]==zPrev[i] && zUuid[i]!=0; i++){} + if( i>=minSize ){ + db_bind_int(&ins, ":cnt", i); + db_bind_text(&ins, ":uuid", zUuid); + db_step(&ins); + db_reset(&ins); + } + sqlite3_snprintf(sizeof(zPrev), zPrev, "%s", zUuid); + } + db_finalize(&ins); + db_finalize(&q); + db_prepare(&q, "SELECT uuid FROM dups ORDER BY length(uuid) DESC, uuid"); + while( db_step(&q)==SQLITE_ROW ){ + fossil_print("%s\n", db_column_text(&q, 0)); + } + db_finalize(&q); +} + +/* +** Schema for the description table +*/ +static const char zDescTab[] = +@ CREATE TEMP TABLE IF NOT EXISTS description( +@ rid INTEGER PRIMARY KEY, -- RID of the object +@ uuid TEXT, -- hash of the object +@ ctime DATETIME, -- Time of creation +@ isPrivate BOOLEAN DEFAULT 0, -- True for unpublished artifacts +@ type TEXT, -- file, check-in, wiki, ticket, etc. +@ rcvid INT, -- When the artifact was received +@ summary TEXT, -- Summary comment for the object +@ ref TEXT -- hash of an object to link against +@ ); +@ CREATE INDEX IF NOT EXISTS desctype +@ ON description(summary) WHERE summary='unknown'; +; + +/* +** Attempt to describe all phantom artifacts. The artifacts are +** already loaded into the description table and have summary='unknown'. +** This routine attempts to generate a better summary, and possibly +** fill in the ref field. +*/ +static void describe_unknown_artifacts(){ + /* Try to figure out the origin of unknown artifacts */ + db_multi_exec( + "REPLACE INTO description(rid,uuid,isPrivate,type,summary,ref)\n" + " SELECT description.rid, description.uuid, isPrivate, type,\n" + " CASE WHEN plink.isprim THEN '' ELSE 'merge ' END ||\n" + " 'parent of check-in', blob.uuid\n" + " FROM description, plink, blob\n" + " WHERE description.summary='unknown'\n" + " AND plink.pid=description.rid\n" + " AND blob.rid=plink.cid;" + ); + db_multi_exec( + "REPLACE INTO description(rid,uuid,isPrivate,type,summary,ref)\n" + " SELECT description.rid, description.uuid, isPrivate, type,\n" + " 'child of check-in', blob.uuid\n" + " FROM description, plink, blob\n" + " WHERE description.summary='unknown'\n" + " AND plink.cid=description.rid\n" + " AND blob.rid=plink.pid;" + ); + db_multi_exec( + "REPLACE INTO description(rid,uuid,isPrivate,type,summary,ref)\n" + " SELECT description.rid, description.uuid, isPrivate, type,\n" + " 'check-in referenced by \"'||tag.tagname ||'\" tag',\n" + " blob.uuid\n" + " FROM description, tagxref, tag, blob\n" + " WHERE description.summary='unknown'\n" + " AND tagxref.origid=description.rid\n" + " AND tag.tagid=tagxref.tagid\n" + " AND blob.rid=tagxref.srcid;" + ); + db_multi_exec( + "REPLACE INTO description(rid,uuid,isPrivate,type,summary,ref)\n" + " SELECT description.rid, description.uuid, isPrivate, type,\n" + " 'file \"'||filename.name||'\"',\n" + " blob.uuid\n" + " FROM description, mlink, filename, blob\n" + " WHERE description.summary='unknown'\n" + " AND mlink.fid=description.rid\n" + " AND blob.rid=mlink.mid\n" + " AND filename.fnid=mlink.fnid;" + ); + if( !db_exists("SELECT 1 FROM description WHERE summary='unknown'") ){ + return; + } + add_content_sql_commands(g.db); + db_multi_exec( + "REPLACE INTO description(rid,uuid,isPrivate,type,summary,ref)\n" + " SELECT description.rid, description.uuid, isPrivate, type,\n" + " 'referenced by cluster', blob.uuid\n" + " FROM description, tagxref, blob\n" + " WHERE description.summary='unknown'\n" + " AND tagxref.tagid=(SELECT tagid FROM tag WHERE tagname='cluster')\n" + " AND blob.rid=tagxref.rid\n" + " AND CAST(content(blob.uuid) AS text)" + " GLOB ('*M '||description.uuid||'*');" + ); +} + +/* +** Create the description table if it does not already exists. +** Populate fields of this table with descriptions for all artifacts +** whose RID matches the SQL expression in zWhere. +*/ +void describe_artifacts(const char *zWhere){ + db_multi_exec("%s", zDescTab/*safe-for-%s*/); + + /* Describe check-ins */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, event.mtime, 'checkin',\n" + " 'check-in to '\n" + " || coalesce((SELECT value FROM tagxref WHERE tagid=%d" + " AND tagtype>0 AND tagxref.rid=blob.rid),'trunk')\n" + " || ' by ' || coalesce(event.euser,event.user)\n" + " || ' on ' || strftime('%%Y-%%m-%%d %%H:%%M',event.mtime)\n" + " FROM event, blob\n" + " WHERE (event.objid %s) AND event.type='ci'\n" + " AND event.objid=blob.rid;", + TAG_BRANCH, zWhere /*safe-for-%s*/ + ); + + /* Describe files */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, event.mtime," + " 'file', 'file '||filename.name\n" + " FROM mlink, blob, event, filename\n" + " WHERE (mlink.fid %s)\n" + " AND mlink.mid=event.objid\n" + " AND filename.fnid=mlink.fnid\n" + " AND mlink.fid=blob.rid;", + zWhere /*safe-for-%s*/ + ); + + /* Describe tags */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, tagxref.mtime, 'tag',\n" + " 'tag '||substr((SELECT uuid FROM blob WHERE rid=tagxref.rid),1,16)\n" + " FROM tagxref, blob\n" + " WHERE (tagxref.srcid %s) AND tagxref.srcid!=tagxref.rid\n" + " AND tagxref.srcid=blob.rid;", + zWhere /*safe-for-%s*/ + ); + + /* Cluster artifacts */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, rcvfrom.mtime," + " 'cluster', 'cluster'\n" + " FROM tagxref, blob, rcvfrom\n" + " WHERE (tagxref.rid %s)\n" + " AND tagxref.tagid=(SELECT tagid FROM tag WHERE tagname='cluster')\n" + " AND blob.rid=tagxref.rid" + " AND rcvfrom.rcvid=blob.rcvid;", + zWhere /*safe-for-%s*/ + ); + + /* Ticket change artifacts */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, tagxref.mtime, 'ticket',\n" + " 'ticket '||substr(tag.tagname,5,21)\n" + " FROM tagxref, tag, blob\n" + " WHERE (tagxref.rid %s)\n" + " AND tag.tagid=tagxref.tagid\n" + " AND tag.tagname GLOB 'tkt-*'" + " AND blob.rid=tagxref.rid;", + zWhere /*safe-for-%s*/ + ); + + /* Wiki edit artifacts */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, tagxref.mtime, 'wiki',\n" + " printf('wiki \"%%s\"',substr(tag.tagname,6))\n" + " FROM tagxref, tag, blob\n" + " WHERE (tagxref.rid %s)\n" + " AND tag.tagid=tagxref.tagid\n" + " AND tag.tagname GLOB 'wiki-*'" + " AND blob.rid=tagxref.rid;", + zWhere /*safe-for-%s*/ + ); + + /* Event edit artifacts */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, tagxref.mtime, 'event',\n" + " 'event '||substr(tag.tagname,7)\n" + " FROM tagxref, tag, blob\n" + " WHERE (tagxref.rid %s)\n" + " AND tag.tagid=tagxref.tagid\n" + " AND tag.tagname GLOB 'event-*'" + " AND blob.rid=tagxref.rid;", + zWhere /*safe-for-%s*/ + ); + + /* Attachments */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, attachment.mtime," + " 'attach-control',\n" + " 'attachment-control for '||attachment.filename\n" + " FROM attachment, blob\n" + " WHERE (attachment.attachid %s)\n" + " AND blob.rid=attachment.attachid", + zWhere /*safe-for-%s*/ + ); + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT blob.rid, blob.uuid, blob.rcvid, attachment.mtime, 'attachment',\n" + " 'attachment '||attachment.filename\n" + " FROM attachment, blob\n" + " WHERE (blob.rid %s)\n" + " AND blob.rid NOT IN (SELECT rid FROM description)\n" + " AND blob.uuid=attachment.src", + zWhere /*safe-for-%s*/ + ); + + /* Forum posts */ + if( db_table_exists("repository","forumpost") ){ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,ctime,type,summary)\n" + "SELECT postblob.rid, postblob.uuid, postblob.rcvid," + " forumpost.fmtime, 'forumpost',\n" + " CASE WHEN fpid=froot THEN 'forum-post '\n" + " ELSE 'forum-reply-to ' END || substr(rootblob.uuid,1,14)\n" + " FROM forumpost, blob AS postblob, blob AS rootblob\n" + " WHERE (forumpost.fpid %s)\n" + " AND postblob.rid=forumpost.fpid" + " AND rootblob.rid=forumpost.froot", + zWhere /*safe-for-%s*/ + ); + } + + /* Mark all other artifacts as "unknown" for now */ + db_multi_exec( + "INSERT OR IGNORE INTO description(rid,uuid,rcvid,type,summary)\n" + "SELECT blob.rid, blob.uuid,blob.rcvid,\n" + " CASE WHEN EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)\n" + " THEN 'phantom' ELSE '' END,\n" + " 'unknown'\n" + " FROM blob\n" + " WHERE (blob.rid %s)\n" + " AND (blob.rid NOT IN (SELECT rid FROM description));", + zWhere /*safe-for-%s*/ + ); + + /* Mark private elements */ + db_multi_exec( + "UPDATE description SET isPrivate=1 WHERE rid IN private" + ); + + if( db_exists("SELECT 1 FROM description WHERE summary='unknown'") ){ + describe_unknown_artifacts(); + } +} + +/* +** Print the content of the description table on stdout. +** +** The description table is computed using the WHERE clause zWhere if +** the zWhere parameter is not NULL. If zWhere is NULL, then this +** routine assumes that the description table already exists and is +** populated and merely prints the contents. +*/ +int describe_artifacts_to_stdout(const char *zWhere, const char *zLabel){ + Stmt q; + int cnt = 0; + if( zWhere!=0 ) describe_artifacts(zWhere); + db_prepare(&q, + "SELECT uuid, summary, coalesce(ref,''), isPrivate\n" + " FROM description\n" + " ORDER BY ctime, type;" + ); + while( db_step(&q)==SQLITE_ROW ){ + if( zLabel ){ + fossil_print("%s\n", zLabel); + zLabel = 0; + } + fossil_print(" %.16s %s %s", db_column_text(&q,0), + db_column_text(&q,1), db_column_text(&q,2)); + if( db_column_int(&q,3) ) fossil_print(" (private)"); + fossil_print("\n"); + cnt++; + } + db_finalize(&q); + if( zWhere!=0 ) db_multi_exec("DELETE FROM description;"); + return cnt; +} + +/* +** COMMAND: test-describe-artifacts +** +** Usage: %fossil test-describe-artifacts [--from S] [--count N] +** +** Display a one-line description of every artifact. +*/ +void test_describe_artifacts_cmd(void){ + int iFrom = 0; + int iCnt = 1000000; + const char *z; + char *zRange; + db_find_and_open_repository(0,0); + z = find_option("from",0,1); + if( z ) iFrom = atoi(z); + z = find_option("count",0,1); + if( z ) iCnt = atoi(z); + zRange = mprintf("BETWEEN %d AND %d", iFrom, iFrom+iCnt-1); + describe_artifacts_to_stdout(zRange, 0); +} + +/* +** WEBPAGE: bloblist +** +** Return a page showing all artifacts in the repository. Query parameters: +** +** n=N Show N artifacts +** s=S Start with artifact number S +** priv Show only unpublished or private artifacts +** phan Show only phantom artifacts +** hclr Color code hash types (SHA1 vs SHA3) +** recent Show the most recent N artifacts +*/ +void bloblist_page(void){ + Stmt q; + int s = atoi(PD("s","0")); + int n = atoi(PD("n","5000")); + int mx = db_int(0, "SELECT max(rid) FROM blob"); + int privOnly = PB("priv"); + int phantomOnly = PB("phan"); + int hashClr = PB("hclr"); + int bRecent = PB("recent"); + int bUnclst = PB("unclustered"); + char *zRange; + char *zSha1Bg; + char *zSha3Bg; + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + cgi_check_for_malice(); + style_header("List Of Artifacts"); + style_submenu_element("250 Largest", "bigbloblist"); + if( bRecent==0 || n!=250 ){ + style_submenu_element("Recent","bloblist?n=250&recent"); + } + if( bUnclst==0 ){ + style_submenu_element("Unclustered","bloblist?unclustered"); + } + if( g.perm.Admin ){ + style_submenu_element("Xfer Log", "rcvfromlist"); + } + if( !phantomOnly ){ + style_submenu_element("Phantoms", "bloblist?phan"); + } + style_submenu_element("Clusters","clusterlist"); + if( g.perm.Private || g.perm.Admin ){ + if( !privOnly ){ + style_submenu_element("Private", "bloblist?priv"); + } + }else{ + privOnly = 0; + } + if( g.perm.Write ){ + style_submenu_element("Artifact Stats", "artifact_stats"); + } + if( !privOnly && !phantomOnly && mx>n && P("s")==0 && !bRecent && !bUnclst ){ + int i; + @

    Select a range of artifacts to view:

    + @
      + for(i=1; i<=mx; i+=n){ + @
    • %z(href("%R/bloblist?s=%d&n=%d",i,n)) + @ %d(i)..%d(i+n-1 + } + @
    • %z(href("%R/bloblist?n=250&recent"))250 most recent + @
    • %z(href("%R/bloblist?unclustered"))All unclustered + @
    + style_finish_page(); + return; + } + if( phantomOnly || privOnly || mx>n ){ + style_submenu_element("Index", "bloblist"); + } + if( privOnly ){ + @

    Private Artifacts

    + zRange = mprintf("IN private"); + }else if( phantomOnly ){ + @

    Phantom Artifacts

    + zRange = mprintf("IN phantom"); + }else if( bUnclst ){ + @

    Unclustered Artifacts

    + zRange = mprintf("IN unclustered"); + }else if( bRecent ){ + @

    %d(n) Most Recent Artifacts

    + zRange = mprintf(">=(SELECT rid FROM blob" + " ORDER BY rid DESC LIMIT 1 OFFSET %d)",n); + }else{ + zRange = mprintf("BETWEEN %d AND %d", s, s+n-1); + } + describe_artifacts(zRange); + fossil_free(zRange); + db_prepare(&q, + /* 0 1 2 3 4 5 6 */ + "SELECT rid, uuid, summary, isPrivate, type='phantom', ref, rcvid, " + " datetime(rcvfrom.mtime)" + " FROM description LEFT JOIN rcvfrom USING(rcvid)" + " ORDER BY rid %s", + ((bRecent||bUnclst)?"DESC":"ASC")/*safe-for-%s*/ + ); + if( skin_detail_boolean("white-foreground") ){ + zSha1Bg = "#714417"; + zSha3Bg = "#177117"; + }else{ + zSha1Bg = "#ebffb0"; + zSha3Bg = "#b0ffb0"; + } + @ + @ + }else{ + @ + } + @ + if( g.perm.Admin ){ + int rcvid = db_column_int(&q, 6); + @ + if( zRef && zRef[0] ){ + @ + }else if( isPhantom==0 ){ + @ + }else{ + @ + } + }else{ + @ + } + @
    RIDHashReceivedDescriptionRefRemarks + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q,0); + const char *zUuid = db_column_text(&q, 1); + const char *zDesc = db_column_text(&q, 2); + int isPriv = db_column_int(&q,3); + int isPhantom = db_column_int(&q,4); + const char *zRef = db_column_text(&q,5); + const char *zDate = db_column_text(&q,7); + if( isPriv && !isPhantom && !g.perm.Private && !g.perm.Admin ){ + /* Don't show private artifacts to users without Private (x) permission */ + continue; + } + if( hashClr ){ + const char *zClr = db_column_bytes(&q,1)>40 ? zSha3Bg : zSha1Bg; + @
    %d(rid)
    %d(rid) %z(href("%R/info/%!S",zUuid))%S(zUuid) %h(zDate) + }else{ + @ %h(zDate) + } + @ %h(zDesc)%z(href("%R/info/%!S",zRef))%S(zRef) + }else{ + @   + } + if( isPriv || isPhantom ){ + if( isPriv==0 ){ + @ phantomprivateprivate,phantom  + } + @
    + db_finalize(&q); + style_finish_page(); +} + +/* +** Output HTML that shows a table of all public phantoms. +*/ +void table_of_public_phantoms(void){ + Stmt q; + char *zRange; + double rNow; + zRange = mprintf("IN (SELECT rid FROM phantom EXCEPT" + " SELECT rid FROM private)"); + describe_artifacts(zRange); + fossil_free(zRange); + db_prepare(&q, + "SELECT rid, uuid, summary, ref," + " (SELECT mtime FROM blob, rcvfrom" + " WHERE blob.uuid=ref AND rcvfrom.rcvid=blob.rcvid)" + " FROM description ORDER BY rid" + ); + rNow = db_double(0.0, "SELECT julianday('now')"); + @ + @ + @ + if( zRef && zRef[0] ){ + @ + } + @
    RIDDescriptionSourceAge + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q,0); + const char *zUuid = db_column_text(&q, 1); + const char *zDesc = db_column_text(&q, 2); + const char *zRef = db_column_text(&q,3); + double mtime = db_column_double(&q,4); + @
    %d(rid)%h(zUuid)
    %h(zDesc)
    %z(href("%R/info/%!S",zRef))%!S(zRef) + if( mtime>0 ){ + char *zAge = human_readable_age(rNow - mtime); + @ %h(zAge) + fossil_free(zAge); + }else{ + @   + } + }else{ + @    + } + @
    + db_finalize(&q); +} + +/* +** WEBPAGE: phantoms +** +** Show a list of all "phantom" artifacts that are not marked as "private". +** +** A "phantom" artifact is an artifact whose hash named appears in some +** artifact but whose content is unknown. For example, if a manifest +** references a particular SHA3 hash of a file, but that SHA3 hash is +** not on the shunning list and is not in the database, then the file +** is a phantom. We know it exists, but we do not know its content. +** +** Whenever a sync occurs, both each party looks at its phantom list +** and for every phantom that is not also marked private, it asks the +** other party to send it the content. This mechanism helps keep all +** repositories synced up. +** +** This page is similar to the /bloblist page in that it lists artifacts. +** But this page is a special case in that it only shows phantoms that +** are not private. In other words, this page shows all phantoms that +** generate extra network traffic on every sync request. +*/ +void phantom_list_page(void){ + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + style_header("Public Phantom Artifacts"); + if( g.perm.Admin ){ + style_submenu_element("Xfer Log", "rcvfromlist"); + style_submenu_element("Artifact List", "bloblist"); + } + if( g.perm.Write ){ + style_submenu_element("Artifact Stats", "artifact_stats"); + } + table_of_public_phantoms(); + style_finish_page(); +} + +/* +** WEBPAGE: bigbloblist +** +** Return a page showing the largest artifacts in the repository in order +** of decreasing size. +** +** n=N Show the top N artifacts (default: 250) +*/ +void bigbloblist_page(void){ + Stmt q; + int n = atoi(PD("n","250")); + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + if( g.perm.Admin ){ + style_submenu_element("Xfer Log", "rcvfromlist"); + } + if( g.perm.Write ){ + style_submenu_element("Artifact Stats", "artifact_stats"); + } + style_submenu_element("All Artifacts", "bloblist"); + style_header("%d Largest Artifacts", n); + db_multi_exec( + "CREATE TEMP TABLE toshow(rid INTEGER PRIMARY KEY);" + "INSERT INTO toshow(rid)" + " SELECT rid FROM blob" + " ORDER BY length(content) DESC" + " LIMIT %d;", n + ); + describe_artifacts("IN toshow"); + db_prepare(&q, + "SELECT description.rid, description.uuid, description.summary," + " length(blob.content), coalesce(delta.srcid,'')," + " datetime(description.ctime)" + " FROM description, blob LEFT JOIN delta ON delta.rid=blob.rid" + " WHERE description.rid=blob.rid" + " ORDER BY length(content) DESC" + ); + @ + @ + @ + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q,0); + const char *zUuid = db_column_text(&q, 1); + const char *zDesc = db_column_text(&q, 2); + int sz = db_column_int(&q,3); + const char *zSrcId = db_column_text(&q,4); + const char *zDate = db_column_text(&q,5); + @ + @ + @ + @ + @ + @ + @ + } + @
    SizeRID + @ FromHashDescriptionDate
    %d(sz)%d(rid)%s(zSrcId) %z(href("%R/info/%!S",zUuid))%S(zUuid) %h(zDesc)%z(href("%R/timeline?c=%T",zDate))%s(zDate)
    + db_finalize(&q); + style_table_sorter(); + style_finish_page(); +} + +/* +** WEBPAGE: deltachain +** +** Usage: /deltachain/RID +** +** The RID query parameter is required. Generate a page with a table +** showing storage characteristics of RID and other artifacts that are +** derived from RID via delta. +*/ +void deltachain_page(void){ + Stmt q; + int id = atoi(PD("name","0")); + int top; + i64 nStored = 0; + i64 nExpanded = 0; + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + top = db_int(id, + "WITH RECURSIVE chain(aa,bb) AS (\n" + " SELECT rid, srcid FROM delta WHERE rid=%d\n" + " UNION ALL\n" + " SELECT bb, delta.srcid" + " FROM chain LEFT JOIN delta ON delta.rid=bb" + " WHERE bb IS NOT NULL\n" + ")\n" + "SELECT aa FROM chain WHERE bb IS NULL", + id + ); + style_header("Delta Chain Containing Artifact %d", id); + db_multi_exec( + "CREATE TEMP TABLE toshow(rid INT, gen INT);\n" + "WITH RECURSIVE tx(id,px) AS (\n" + " VALUES(%d,0)\n" + " UNION ALL\n" + " SELECT delta.rid, px+1 FROM tx, delta where delta.srcid=tx.id\n" + " ORDER BY 2\n" + ") " + "INSERT INTO toshow(rid,gen) SELECT id,px FROM tx;", + top + ); + db_multi_exec("CREATE INDEX toshow_rid ON toshow(rid);"); + describe_artifacts("IN (SELECT rid FROM toshow)"); + db_prepare(&q, + "SELECT description.rid, description.uuid, description.summary," + " length(blob.content), coalesce(delta.srcid,'')," + " datetime(description.ctime), toshow.gen, blob.size" + " FROM description, toshow, blob LEFT JOIN delta ON delta.rid=blob.rid" + " WHERE description.rid=blob.rid" + " AND toshow.rid=description.rid" + " ORDER BY toshow.gen, description.ctime" + ); + @ + @ + @ + @ + while( db_step(&q)==SQLITE_ROW ){ + int rid = db_column_int(&q,0); + const char *zUuid = db_column_text(&q, 1); + const char *zDesc = db_column_text(&q, 2); + int sz = db_column_int(&q,3); + const char *zSrcId = db_column_text(&q,4); + const char *zDate = db_column_text(&q,5); + int gen = db_column_int(&q,6); + nExpanded += db_column_int(&q,7); + nStored += sz; + @ + @ + if( rid==id ){ + @ + }else{ + @ + } + @ + @ + @ + @ + @ + } + @
    LevelSizeRID + @ FromHashDescriptionDate
    %d(gen)%d(sz)%d(rid)%d(rid)%s(zSrcId) %z(href("%R/info/%!S",zUuid))%S(zUuid) %h(zDesc)%z(href("%R/timeline?c=%T",zDate))%s(zDate)
    + db_finalize(&q); + style_table_sorter(); + @

    + @ + @ + @ + @ + @ + @
    Bytes of content   %,lld(nExpanded)
    Bytes stored in repository%,lld(nStored)
    + @

    + style_finish_page(); +} + +/* +** COMMAND: test-unsent +** +** Usage: %fossil test-unsent +** +** Show all artifacts in the unsent table +*/ +void test_unsent_cmd(void){ + db_find_and_open_repository(0,0); + describe_artifacts_to_stdout("IN unsent", 0); +} + +/* +** COMMAND: test-unclustered +** +** Usage: %fossil test-unclustered +** +** Show all artifacts in the unclustered table +*/ +void test_unclusterd_cmd(void){ + db_find_and_open_repository(0,0); + describe_artifacts_to_stdout("IN unclustered", 0); +} + +/* +** COMMAND: test-phantoms +** +** Usage: %fossil test-phantoms +** +** Show all phantom artifacts. A phantom artifact is one for which there +** is no content. Options: +** +** --count Show only a count of the number of phantoms. +** --delta Show all delta-phantoms. A delta-phantom is a +** artifact for which there is a delta but the delta +** source is a phantom. +** --list Just list the phantoms. Do not try to describe them. +*/ +void test_phantoms_cmd(void){ + int bDelta; + int bList; + int bCount; + unsigned nPhantom = 0; + unsigned nDeltaPhantom = 0; + db_find_and_open_repository(0,0); + bDelta = find_option("delta", 0, 0)!=0; + bList = find_option("list", 0, 0)!=0; + bCount = find_option("count", 0, 0)!=0; + verify_all_options(); + if( bList || bCount ){ + Stmt q1, q2; + db_prepare(&q1, "SELECT rid, uuid FROM blob WHERE size<0"); + while( db_step(&q1)==SQLITE_ROW ){ + int rid = db_column_int(&q1, 0); + nPhantom++; + if( !bCount ){ + fossil_print("%S (%d)\n", db_column_text(&q1,1), rid); + } + db_prepare(&q2, + "WITH RECURSIVE deltasof(rid) AS (" + " SELECT rid FROM delta WHERE srcid=%d" + " UNION" + " SELECT delta.rid FROM deltasof, delta" + " WHERE delta.srcid=deltasof.rid)" + "SELECT deltasof.rid, blob.uuid FROM deltasof LEFT JOIN blob" + " ON blob.rid=deltasof.rid", rid + ); + while( db_step(&q2)==SQLITE_ROW ){ + nDeltaPhantom++; + if( !bCount ){ + fossil_print(" %S (%d)\n", db_column_text(&q2,1), + db_column_int(&q2,0)); + } + } + db_finalize(&q2); + } + db_finalize(&q1); + if( nPhantom ){ + fossil_print("Phantoms: %u Delta-phantoms: %u\n", + nPhantom, nDeltaPhantom); + } + }else if( bDelta ){ + describe_artifacts_to_stdout( + "IN (WITH RECURSIVE delta_phantom(rid) AS (\n" + " SELECT delta.rid FROM blob, delta\n" + " WHERE blob.size<0 AND delta.srcid=blob.rid\n" + " UNION\n" + " SELECT delta.rid FROM delta_phantom, delta\n" + " WHERE delta.srcid=delta_phantom.rid)\n" + " SELECT rid FROM delta_phantom)", 0); + }else{ + describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0); + } +} + +/* Maximum number of collision examples to remember */ +#define MAX_COLLIDE 25 + +/* +** Generate a report on the number of collisions in artifact hashes +** generated by the SQL given in the argument. +*/ +static void collision_report(const char *zSql){ + int i, j; + int nHash = 0; + Stmt q; + char zPrev[HNAME_MAX+1]; + struct { + int cnt; + char *azHit[MAX_COLLIDE]; + char z[HNAME_MAX+1]; + } aCollide[HNAME_MAX+1]; + memset(aCollide, 0, sizeof(aCollide)); + memset(zPrev, 0, sizeof(zPrev)); + db_prepare(&q,"%s",zSql/*safe-for-%s*/); + while( db_step(&q)==SQLITE_ROW ){ + const char *zUuid = db_column_text(&q,0); + int n = db_column_bytes(&q,0); + int i; + nHash++; + for(i=0; zPrev[i] && zPrev[i]==zUuid[i]; i++){} + if( i>0 && i<=HNAME_MAX ){ + if( i>=4 && aCollide[i].cnt + @ LengthInstancesFirst Instance + @ + for(i=1; i<=HNAME_MAX; i++){ + if( aCollide[i].cnt==0 ) continue; + @ %d(i)%d(aCollide[i].cnt)%h(aCollide[i].z) + } + @ + @

    Total number of hashes: %d(nHash)

    + for(i=HNAME_MAX; i>=4; i--){ + if( aCollide[i].cnt==0 ) continue; + if( aCollide[i].cnt>200 ) break; + if( aCollide[i].cnt<25 ){ + @

    Collisions of length %d(i): + }else{ + @

    First 25 collisions of length %d(i): + } + for(j=0; j + } + } + for(i=4; iHash Prefix Collisions on Check-ins + collision_report("SELECT (SELECT uuid FROM blob WHERE rid=objid)" + " FROM event WHERE event.type='ci'" + " ORDER BY 1"); + @

    Hash Prefix Collisions on All Artifacts

    + collision_report("SELECT uuid FROM blob ORDER BY 1"); + style_finish_page(); +} + +/* +** WEBPAGE: clusterlist +** +** Show information about all cluster artifacts in the database. +*/ +void clusterlist_page(void){ + Stmt q; + int cnt = 1; + sqlite3_int64 szTotal = 0; + sqlite3_int64 szCTotal = 0; + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + style_header("All Cluster Artifacts"); + style_submenu_element("All Artifactst", "bloblist"); + if( g.perm.Admin ){ + style_submenu_element("Xfer Log", "rcvfromlist"); + } + style_submenu_element("Phantoms", "bloblist?phan"); + if( g.perm.Write ){ + style_submenu_element("Artifact Stats", "artifact_stats"); + } + + db_prepare(&q, + "SELECT blob.uuid, " + " blob.size, " + " octet_length(blob.content), " + " datetime(rcvfrom.mtime)," + " user.login," + " rcvfrom.ipaddr" + " FROM tagxref JOIN blob ON tagxref.rid=blob.rid" + " LEFT JOIN rcvfrom ON blob.rcvid=rcvfrom.rcvid" + " LEFT JOIN user ON user.uid=rcvfrom.uid" + " WHERE tagxref.tagid=%d" + " ORDER BY rcvfrom.mtime, blob.uuid", + TAG_CLUSTER + ); + @ + @ + } + @
      + @ Hash + @ Date Received + @ Size + @ Compressed Size + if( g.perm.Admin ){ + @ UserIP-Address + } + while( db_step(&q)==SQLITE_ROW ){ + const char *zUuid = db_column_text(&q, 0); + sqlite3_int64 sz = db_column_int64(&q, 1); + sqlite3_int64 szC = db_column_int64(&q, 2); + const char *zDate = db_column_text(&q, 3); + const char *zUser = db_column_text(&q, 4); + const char *zIp = db_column_text(&q, 5); + szTotal += sz; + szCTotal += szC; + @
    %d(cnt++) + @ %S(zUuid) + if( zDate ){ + @ %h(zDate) + }else{ + @   + } + @ %,lld(sz) + @ %,lld(szC) + if( g.perm.Admin ){ + if( zUser ){ + @ %h(zUser) + }else{ + @   + } + if( zIp ){ + @ %h(zIp) + }else{ + @   + } + } + @
    + db_finalize(&q); + @

    Total size of all clusters: %,lld(szTotal) bytes, + @ %,lld(szCTotal) bytes compressed

    + style_finish_page(); +} -- cgit v1.3.1