From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/tag.c | 1072 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1072 insertions(+) create mode 100644 src/tag.c (limited to 'src/tag.c') diff --git a/src/tag.c b/src/tag.c new file mode 100644 index 0000000..c1be5a3 --- /dev/null +++ b/src/tag.c @@ -0,0 +1,1072 @@ +/* +** 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 used to manage tags +*/ +#include "config.h" +#include "tag.h" +#include + +/* +** Propagate the tag given by tagid to the children of pid. +** +** This routine assumes that tagid is a tag that should be +** propagated and that the tag is already present in pid. +** +** If tagtype is 2 then the tag is being propagated from an +** ancestor node. If tagtype is 0 it means a propagating tag is +** being blocked. +*/ +static void tag_propagate( + int pid, /* Propagate the tag to children of this node */ + int tagid, /* Tag to propagate */ + int tagType, /* 2 for a propagating tag. 0 for an antitag */ + int origId, /* Artifact of tag, when tagType==2 */ + const char *zValue, /* Value of the tag. Might be NULL */ + double mtime /* Timestamp on the tag */ +){ + PQueue queue; /* Queue of check-ins to be tagged */ + Stmt s; /* Query the children of :pid to which to propagate */ + Stmt ins; /* INSERT INTO tagxref */ + Stmt eventupdate; /* UPDATE event */ + + assert( tagType==0 || tagType==2 ); + pqueuex_init(&queue); + pqueuex_insert(&queue, pid, 0.0); + + /* Query for children of :pid to which to propagate the tag. + ** Three returns: (1) rid of the child. (2) timestamp of child. + ** (3) True to propagate or false to block. + */ + db_prepare(&s, + "SELECT cid, plink.mtime," + " coalesce(srcid=0 AND tagxref.mtime<:mtime, %d) AS doit" + " FROM plink LEFT JOIN tagxref ON cid=rid AND tagid=%d" + " WHERE pid=:pid AND isprim", + tagType==2, tagid + ); + db_bind_double(&s, ":mtime", mtime); + + if( tagType==2 ){ + /* Set the propagated tag marker on check-in :rid */ + db_prepare(&ins, + "REPLACE INTO tagxref(tagid, tagtype, srcid, origid, value, mtime, rid)" + "VALUES(%d,2,0,%d,%Q,:mtime,:rid)", + tagid, origId, zValue + ); + db_bind_double(&ins, ":mtime", mtime); + }else{ + /* Remove all references to the tag from check-in :rid */ + zValue = 0; + db_prepare(&ins, + "DELETE FROM tagxref WHERE tagid=%d AND rid=:rid", tagid + ); + } + if( tagid==TAG_BGCOLOR ){ + db_prepare(&eventupdate, + "UPDATE event SET bgcolor=%Q WHERE objid=:rid", zValue + ); + } + while( (pid = pqueuex_extract(&queue))!=0 ){ + db_bind_int(&s, ":pid", pid); + while( db_step(&s)==SQLITE_ROW ){ + int doit = db_column_int(&s, 2); + if( doit ){ + int cid = db_column_int(&s, 0); + double mtime = db_column_double(&s, 1); + pqueuex_insert(&queue, cid, mtime); + db_bind_int(&ins, ":rid", cid); + db_step(&ins); + db_reset(&ins); + if( tagid==TAG_BGCOLOR ){ + db_bind_int(&eventupdate, ":rid", cid); + db_step(&eventupdate); + db_reset(&eventupdate); + } + if( tagid==TAG_BRANCH ){ + leaf_eventually_check(cid); + } + } + } + db_reset(&s); + } + pqueuex_clear(&queue); + db_finalize(&ins); + db_finalize(&s); + if( tagid==TAG_BGCOLOR ){ + db_finalize(&eventupdate); + } +} + +/* +** Propagate all propagatable tags in pid to the children of pid. +*/ +void tag_propagate_all(int pid){ + Stmt q; + db_prepare(&q, + "SELECT tagid, tagtype, mtime, value, origid FROM tagxref" + " WHERE rid=%d", + pid + ); + while( db_step(&q)==SQLITE_ROW ){ + int tagid = db_column_int(&q, 0); + int tagtype = db_column_int(&q, 1); + double mtime = db_column_double(&q, 2); + const char *zValue = db_column_text(&q, 3); + int origid = db_column_int(&q, 4); + if( tagtype==1 ) tagtype = 0; + tag_propagate(pid, tagid, tagtype, origid, zValue, mtime); + } + db_finalize(&q); +} + +/* +** Get a tagid for the given TAG. Create a new tag if necessary +** if createFlag is 1. +*/ +int tag_findid(const char *zTag, int createFlag){ + int id; + id = db_int(0, "SELECT tagid FROM tag WHERE tagname=%Q", zTag); + if( id==0 && createFlag ){ + db_multi_exec("INSERT INTO tag(tagname) VALUES(%Q)", zTag); + id = db_last_insert_rowid(); + } + return id; +} + +/* +** Insert a tag into the database. +** +** Also translate zTag into a tagid and return the tagid. (In other words +** if zTag is "bgcolor" then return TAG_BGCOLOR.) +*/ +int tag_insert( + const char *zTag, /* Name of the tag (w/o the "+" or "-" prefix */ + int tagtype, /* 0:cancel 1:singleton 2:propagated */ + const char *zValue, /* Value if the tag is really a property */ + int srcId, /* Artifact that contains this tag */ + double mtime, /* Timestamp. Use default if <=0.0 */ + int rid /* Artifact to which the tag is to attached */ +){ + Stmt s; + const char *zCol; + int tagid = tag_findid(zTag, 1); + int rc; + + if( mtime<=0.0 ){ + mtime = db_double(0.0, "SELECT julianday('now')"); + } + db_prepare(&s, + "SELECT 1 FROM tagxref" + " WHERE tagid=%d" + " AND rid=%d" + " AND mtime>=:mtime", + tagid, rid + ); + db_bind_double(&s, ":mtime", mtime); + rc = db_step(&s); + db_finalize(&s); + if( rc==SQLITE_ROW ){ + /* Another entry that is more recent already exists. Do nothing */ + return tagid; + } + db_prepare(&s, + "REPLACE INTO tagxref(tagid,tagtype,srcId,origid,value,mtime,rid)" + " VALUES(%d,%d,%d,%d,%Q,:mtime,%d)", + tagid, tagtype, srcId, rid, zValue, rid + ); + db_bind_double(&s, ":mtime", mtime); + db_step(&s); + db_finalize(&s); + if( tagid==TAG_BRANCH ) leaf_eventually_check(rid); + if( tagtype==0 ){ + zValue = 0; + } + zCol = 0; + switch( tagid ){ + case TAG_BGCOLOR: { + zCol = "bgcolor"; + break; + } + case TAG_COMMENT: { + zCol = "ecomment"; + break; + } + case TAG_USER: { + zCol = "euser"; + break; + } + case TAG_PRIVATE: { + db_multi_exec( + "INSERT OR IGNORE INTO private(rid) VALUES(%d);", + rid + ); + } + } + if( zCol ){ + db_multi_exec("UPDATE event SET \"%w\"=%Q WHERE objid=%d", + zCol, zValue, rid); + if( tagid==TAG_COMMENT && zValue!=0 ){ + char *zCopy = fossil_strdup(zValue); + backlink_extract(zCopy, MT_NONE, rid, BKLNK_COMMENT, mtime, 1); + free(zCopy); + } + } + if( tagid==TAG_DATE ){ + db_multi_exec("UPDATE event " + " SET mtime=julianday(%Q)," + " omtime=coalesce(omtime,mtime)" + " WHERE objid=%d", + zValue, rid); + } + if( tagid==TAG_PARENT && tagtype==1 ){ + manifest_reparent_checkin(rid, zValue); + } + if( tagtype==1 ) tagtype = 0; + tag_propagate(rid, tagid, tagtype, rid, zValue, mtime); + return tagid; +} + + +/* +** COMMAND: test-tag +** +** Usage: %fossil test-tag (+|*|-)TAGNAME ARTIFACT-ID ?VALUE? +** +** Add a tag or anti-tag to the rebuildable tables of the local repository. +** No tag artifact is created so the new tag is erased the next +** time the repository is rebuilt. This routine is for testing +** use only. +*/ +void testtag_cmd(void){ + const char *zTag; + const char *zValue; + int rid; + int tagtype; + db_must_be_within_tree(); + if( g.argc!=4 && g.argc!=5 ){ + usage("TAGNAME ARTIFACT-ID ?VALUE?"); + } + zTag = g.argv[2]; + switch( zTag[0] ){ + case '+': tagtype = 1; break; + case '*': tagtype = 2; break; + case '-': tagtype = 0; break; + default: + fossil_fatal("tag should begin with '+', '*', or '-'"); + return; + } + rid = name_to_rid(g.argv[3]); + if( rid==0 ){ + fossil_fatal("no such object: %s", g.argv[3]); + } + g.markPrivate = content_is_private(rid); + zValue = g.argc==5 ? g.argv[4] : 0; + db_begin_transaction(); + tag_insert(zTag, tagtype, zValue, -1, 0.0, rid); + db_end_transaction(0); +} + +/* +** OR this value into the tagtype argument to tag_add_artifact to +** cause the tag to be displayed on standard output rather than be +** inserted. Used for --dry-run options and debugging. +*/ +#if INTERFACE +#define TAG_ADD_DRYRUN 0x04 +#endif + +/* +** Add a control record to the repository that either creates +** or cancels a tag. +** +** tagtype should normally be 0, 1, or 2. But if the TAG_ADD_DRYRUN bit +** is also set, then simply print the text of the tag on standard output +** (for testing purposes) rather than create the tag. +*/ +void tag_add_artifact( + const char *zPrefix, /* Prefix to prepend to tag name */ + const char *zTagname, /* The tag to add or cancel */ + const char *zObjName, /* Name of object attached to */ + const char *zValue, /* Value for the tag. Might be NULL */ + int tagtype, /* 0:cancel 1:singleton 2:propagated */ + const char *zDateOvrd, /* Override date string */ + const char *zUserOvrd /* Override user name */ +){ + int rid; + int nrid; + char *zDate; + Blob uuid; + Blob ctrl; + Blob cksum; + static const char zTagtype[] = { '-', '+', '*' }; + int dryRun = 0; + + if( tagtype & TAG_ADD_DRYRUN ){ + tagtype &= ~TAG_ADD_DRYRUN; + dryRun = 1; + } + assert( tagtype>=0 && tagtype<=2 ); + user_select(); + blob_zero(&uuid); + blob_append(&uuid, zObjName, -1); + if( name_to_uuid(&uuid, 9, "*") ){ + fossil_fatal("%s", g.zErrMsg); + return; + } + rid = name_to_rid(blob_str(&uuid)); + g.markPrivate = content_is_private(rid); + blob_zero(&ctrl); + +#if 0 + if( validate16(zTagname, strlen(zTagname)) ){ + fossil_fatal( + "invalid tag name \"%s\" - might be confused with" + " a hexadecimal artifact ID", + zTagname + ); + } +#endif + zDate = date_in_standard_format(zDateOvrd ? zDateOvrd : "now"); + blob_appendf(&ctrl, "D %s\n", zDate); + blob_appendf(&ctrl, "T %c%s%F %b", + zTagtype[tagtype], zPrefix, zTagname, &uuid); + if( tagtype>0 && zValue && zValue[0] ){ + blob_appendf(&ctrl, " %F\n", zValue); + }else{ + blob_appendf(&ctrl, "\n"); + } + blob_appendf(&ctrl, "U %F\n", zUserOvrd ? zUserOvrd : login_name()); + md5sum_blob(&ctrl, &cksum); + blob_appendf(&ctrl, "Z %b\n", &cksum); + if( dryRun ){ + fossil_print("%s", blob_str(&ctrl)); + blob_reset(&ctrl); + }else{ + nrid = content_put(&ctrl); + manifest_crosslink(nrid, &ctrl, MC_PERMIT_HOOKS); + } + assert( blob_is_reset(&ctrl) ); + if( g.localOpen ){ + manifest_to_disk(rid); + } +} + +/* +** If zTag is NULL or valid for use as a tag for the `tag add` and +** `tag cancel` commands, returns without side effects, else emits a +** fatal error message. We reject certain prefixes to avoid that +** clients cause undue grief by improperly tagging artifacts as being, +** e.g., wiki pages or tickets. +** +** Note that we intentionally allow the "sym-" prefix, partly for +** historical compatibility and partly because it can be applied +** properly, whereas the other reserved name types have special +** meanings for fossil and cannot be sensibly manually manipulated. +*/ +static void tag_cmd_tagname_check(const char *zTag){ + if(zTag && *zTag && + (strncmp(zTag,"wiki-",5)==0 + || strncmp(zTag,"tkt-",4)==0 + || strncmp(zTag,"event-",6)==0)){ + fossil_fatal("Invalid prefix for tag name: %s", zTag); + } +} + +/* +** Internal helper for the tag command. Fetches rid_root_parent(rid) +** and if it differs from the input, assigns *zSym to the root object's +** UUID (intentionally leaking it). +*/ +static int tag_cmd_root(int rid, char const **zSym, int bVerbose){ + const int origRid = rid; + rid = rid_root_parent(rid); + assert( rid>0 ); + if( origRid!=rid ){ + char const *zOrig = *zSym; + *zSym = rid_to_uuid(rid)/*intentional leak*/; + if( bVerbose ){ + fossil_print("Redirecting tag from %!S to root ancestor %!S.\n", + zOrig, *zSym); + } + } + return rid; +} + +/* +** COMMAND: tag +** +** Usage: %fossil tag SUBCOMMAND ... +** +** Run various subcommands to control tags and properties. +** +** > fossil tag add ?OPTIONS? TAGNAME ARTIFACT-ID ?VALUE? +** +** Add a new tag or property to an artifact referenced by +** ARTIFACT-ID. For check-ins, the tag will be usable instead +** of a CHECK-IN in commands such as update and merge. If the +** --propagate flag is present and ARTIFACT-ID refers to a +** wiki page, forum post, technote, or check-in, the tag +** propagates to all descendants of that artifact. +** +** Options: +** --date-override DATETIME Set date and time added +** -n|--dry-run Display the tag text, but do not +** actually insert it into the database +** --propagate Propagating tag +** --raw Raw tag name. Ignored for +** non-CHECK-IN artifacts. +** --user-override USER Name USER when adding the tag +** --root If ARTIFACT-ID refers to an artifact +** with a P-card, it gets translated to the +** oldest parent of that artifact and +** --propagate is assumed. This is +** most useful with forum posts, wiki +** pages, and tech notes, where tags are +** most easliy managed via their initial +** version. +** +** The --date-override and --user-override options support +** importing history from other SCM systems. DATETIME has +** the form 'YYYY-MM-DD HH:MM:SS'. +** +** Note that fossil uses some tag prefixes internally and this +** command will reject tags with these prefixes to avoid +** causing problems or confusion: "wiki-", "tkt-", "event-". +** +** When tagging forum posts, the ARTIFACT-ID is translated to +** the initial version of that post and --propagate is implied. +** +** > fossil tag cancel ?--raw? TAGNAME ARTIFACT-ID +** +** Remove the tag TAGNAME from the artifact referenced by +** ARTIFACT-ID, and also remove the propagation of the tag to +** any descendants. Use the -n|--dry-run option to see +** what would have happened. Certain tag name prefixes are +** forbidden, as documented for the 'add' subcommand. +** +** Options: +** --date-override DATETIME Set date and time deleted +** -n|--dry-run Display the control artifact, but do +** not insert it into the database +** --raw Raw tag name. Ignored for +** non-CHECK-IN artifacts. +** --user-override USER Name USER when deleting the tag +** --root As described for 'add'. +** +** > fossil tag find ?OPTIONS? TAGNAME +** +** List all objects that use TAGNAME. +** +** Options: +** -n|--limit N Limit to N results +** --raw Interprets tag as a raw name instead of a +** branch name and matches any type of artifact. +** Changes the output to include only the +** hashes of matching objects. +** -t|--type TYPE One of: ci (check-in), w (wiki), +** e (event/technote), f (forum post), +** t (ticket). Default is all types. Ignored +** if --raw is used. +** +** > fossil tag list|ls ?OPTIONS? ?ARTIFACT-ID? +** +** List all tags or, if ARTIFACT-ID is supplied, all tags and +** their values for that artifact. For historical +** scripting compatibility, the internal tag types "wiki-", +** "tkt-", and "event-" (technote) are elided by default +** unless the --raw or --prefix options are used. +** +** Options: +** -v|--inverse Inverse the meaning of --tagtype TYPE +** --prefix List only tags with the given prefix +** Fossil-internal prefixes include "sym-" +** (branch name), "wiki-", "event-" +** (technote), and "tkt-" (ticket). The +** prefix is stripped from the resulting +** list unless --raw is provided. Ignored if +** ARTIFACT-ID is provided. +** --raw List raw names of tags +** --sep SEP Separator when concatenating values +** --tagtype TYPE List only tags of type TYPE, which must +** be one of: cancel, singleton, propagated +** --values List tag values +** If --sep is supplied, list all values of a tag on +** the same line, separated by SEP; otherwise list +** each value on its own line. +** --root As described for 'add' and only if ARTIFACT-ID is +** provided. +** +** The option --raw allows the manipulation of all types of tags +** used for various internal purposes in fossil. It also shows +** "cancel" tags for the "find" and "list" subcommands. You should +** not use this option to make changes unless you are sure what +** you are doing. +** +** If you need to use a tagname that might be confused with +** a hexadecimal baseline or artifact ID, you can explicitly +** disambiguate it by prefixing it with "tag:". For instance: +** +** fossil update decaf +** +** will be taken as an artifact or baseline ID and fossil will +** probably complain that no such revision was found. However +** +** fossil update tag:decaf +** +** will assume that "decaf" is a tag/branch name. +** +*/ +void tag_cmd(void){ + int n; + + db_find_and_open_repository(0, 0); + if( g.argc<3 ){ + goto tag_cmd_usage; + } + n = strlen(g.argv[2]); + if( n==0 ){ + goto tag_cmd_usage; + } + + if( strncmp(g.argv[2],"add",n)==0 ){ + char *zValue; + int dryRun = 0; + int fRaw = find_option("raw","",0)!=0; + int fPropagate = find_option("propagate","",0)!=0; + int fRoot = find_option("root","",0)!=0; + int objType; + int rid; + const char *zPrefix = ""; + const char *zDateOvrd = find_option("date-override",0,1); + const char *zUserOvrd = find_option("user-override",0,1); + const char *zTag; + const char *zObjId; + if( find_option("dry-run","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN; + if( g.argc!=5 && g.argc!=6 ){ + usage("add ?options? TAGNAME ARTIFACT-ID ?VALUE?"); + } + zTag = g.argv[3]; + tag_cmd_tagname_check(zTag); + zObjId = g.argv[4]; + zValue = g.argc==6 ? g.argv[5] : 0; + rid = symbolic_name_to_rid(zObjId, 0); + if( rid<=0 ){ + fossil_fatal("Cannot resolve artifact ID: %s", zObjId); + } + if( fRoot ){ + rid = tag_cmd_root(rid, &zObjId, dryRun); + fPropagate = 1; + } + objType = whatis_rid_type(rid); + if( CFTYPE_MANIFEST==objType ){ + zPrefix = fRaw ? "" : "sym-"; + } + db_begin_transaction(); + tag_add_artifact(zPrefix, zTag, zObjId, zValue, + 1+fPropagate+dryRun,zDateOvrd,zUserOvrd); + db_end_transaction(0); + }else + + if( strncmp(g.argv[2],"branch",n)==0 ){ + fossil_fatal("the \"fossil tag branch\" command is discontinued\n" + "Use the \"fossil branch new\" command instead."); + }else + + if( strncmp(g.argv[2],"cancel",n)==0 ){ + int dryRun = 0; + int fRaw = find_option("raw","",0)!=0; + int fRoot = find_option("root","",0)!=0; + int objType; + int rid; + const char *zPrefix = ""; + const char *zDateOvrd = find_option("date-override",0,1); + const char *zUserOvrd = find_option("user-override",0,1); + const char *zTag; + const char *zObjId; + if( find_option("dry-run","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN; + if( g.argc!=5 ){ + usage("cancel ?options? TAGNAME ARTIFACT-ID"); + } + zTag = g.argv[3]; + tag_cmd_tagname_check(zTag); + zObjId = g.argv[4]; + rid = symbolic_name_to_rid(zObjId, 0); + if( rid<=0 ){ + fossil_fatal("Cannot resolve artifact ID: %s", zObjId); + } + if( fRoot ){ + rid = tag_cmd_root(rid, &zObjId, dryRun); + } + objType = whatis_rid_type(rid); + if( CFTYPE_MANIFEST==objType ){ + zPrefix = fRaw ? "" : "sym-"; + } + db_begin_transaction(); + tag_add_artifact(zPrefix, zTag, zObjId, 0, dryRun, + zDateOvrd, zUserOvrd); + db_end_transaction(0); + }else + + if( strncmp(g.argv[2],"find",n)==0 ){ + Stmt q; + int fRaw = find_option("raw","",0)!=0; + const char *zFindLimit = find_option("limit","n",1); + const int nFindLimit = zFindLimit ? atoi(zFindLimit) : -2000; + const char *zType = find_option("type","t",1); + Blob sql = empty_blob; + const char *zTag; + if( zType==0 || zType[0]==0 ) zType = "*"; + if( g.argc!=4 ){ + usage("find ?--raw? ?-t|--type TYPE? ?-n|--limit #? TAGNAME"); + } + zTag = g.argv[3]; + if( fRaw ){ + blob_append_sql(&sql, + "SELECT blob.uuid FROM tagxref, blob" + " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" + " AND tagxref.tagtype>0" + " AND blob.rid=tagxref.rid", + zTag + ); + if( nFindLimit>0 ){ + blob_append_sql(&sql, " LIMIT %d", nFindLimit); + } + db_prepare(&q, "%s", blob_sql_text(&sql)); + blob_reset(&sql); + while( db_step(&q)==SQLITE_ROW ){ + fossil_print("%s\n", db_column_text(&q, 0)); + } + db_finalize(&q); + }else{ + blob_append_sql(&sql, + "%s" + " AND event.type GLOB '%q'" + " AND blob.rid IN (" + " SELECT rid FROM tagxref" + " WHERE tagtype>0 AND tagid IN (" + " SELECT tagid FROM tag WHERE tagname IN " + " ('%q','sym-%q','wiki-%q','tkt-%q','event-%q')" + " )" + ")" + " ORDER BY event.mtime DESC /*sort*/", + timeline_query_for_tty(), zType, zTag, zTag, zTag, zTag, zTag + ); + db_prepare(&q, "%s", blob_sql_text(&sql)); + blob_reset(&sql); + print_timeline(&q, nFindLimit, 79, 0, 0); + db_finalize(&q); + } + }else + + if(( strncmp(g.argv[2],"list",n)==0 )||( strncmp(g.argv[2],"ls",n)==0 )){ + Stmt q; + const int fRaw = find_option("raw","",0)!=0; + const char *zTagType = find_option("tagtype","t",1); + const int fInverse = find_option("inverse","v",0)!=0; + const char *zTagPrefix = find_option("prefix","",1); + int nTagType = fRaw ? -1 : 0; + int fValues = find_option("values","",0)!=0; + int fRoot = find_option("root","",0)!=0; + const char *zSep = find_option("sep","",1); + + + if( zTagType!=0 ){ + int l = strlen(zTagType); + if( strncmp(zTagType,"cancel",l)==0 ){ + nTagType = 0; + }else if( strncmp(zTagType,"singleton",l)==0 ){ + nTagType = 1; + }else if( strncmp(zTagType,"propagated",l)==0 ){ + nTagType = 2; + }else{ + fossil_fatal("unrecognized tag type"); + } + } + if( g.argc==3 ){ + const int nTagPrefix = zTagPrefix ? (int)strlen(zTagPrefix) : 0; + if( !fValues ){ + db_prepare(&q, + "SELECT tagname FROM tag" + " WHERE EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=tag.tagid" + " AND tagtype%s%d)" + " AND CASE WHEN %Q IS NULL THEN 1 ELSE tagname GLOB %Q||'*' " + " END ORDER BY tagname COLLATE uintnocase", + zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, + nTagType, zTagPrefix, zTagPrefix + ); + }else{ + if( zSep ){ + db_prepare(&q, + /* work around group_concat() with DISTINCT and custom separator */ + "SELECT tagname," + " rtrim(replace(group_concat(DISTINCT value||'@!' " + " ORDER BY value ASC), '@!,', %Q),'@!')" + " FROM tagxref, tag" + " WHERE tagxref.tagid=tag.tagid AND tagtype%s%d" + " AND CASE WHEN %Q IS NULL THEN 1 ELSE tagname GLOB %Q||'*' END" + " GROUP BY tagname" + " ORDER BY tagname COLLATE uintnocase", + ( zSep && strlen(zSep)>0 ) ? zSep : ",", + zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, + nTagType, zTagPrefix, zTagPrefix + ); + }else{ + db_prepare(&q, + "SELECT DISTINCT tagname, value" + " FROM tagxref, tag" + " WHERE tagxref.tagid=tag.tagid AND tagtype%s%d" + " AND CASE WHEN %Q IS NULL THEN 1 ELSE tagname GLOB %Q||'*' END" + " ORDER BY tagname, value COLLATE uintnocase", + zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, + nTagType, zTagPrefix, zTagPrefix + ); + } + } + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q, 0); + const char *zValue = db_column_text(&q, 1); + int nWidth = fValues ? 20 : 0; + const char *zzValue = (fValues && zValue) ? mprintf(" %s", zValue) : ""; + if( fRaw ){ + fossil_print("%-*s%s\n", nWidth, zName, zzValue); + }else if( nTagPrefix>0 ){ + assert(db_column_bytes(&q,0)>=nTagPrefix); + fossil_print("%-*s%s\n", nWidth, &zName[nTagPrefix], zzValue); + }else if( strncmp(zName, "sym-", 4)==0 ){ + fossil_print("%-*s%s\n", nWidth, &zName[4], zzValue); + } + } + db_finalize(&q); + }else if( g.argc==4 ){ + char const *zObjId = g.argv[3]; + int rid = name_to_rid(zObjId); + int objType; + int nTagOffset = 0; + + if( fRoot ){ + rid = tag_cmd_root(rid, &zObjId, 0); + } + zTagPrefix = 0; + objType = rid>=0 ? whatis_rid_type(rid) : 0; + if(objType<=0){ + fossil_fatal("Cannot resolve artifact ID: %s", zObjId); + }else if(fRaw==0){ + /* Figure out the tag prefix to strip */ + switch(objType){ + case CFTYPE_MANIFEST: zTagPrefix = "sym-"; break; + case CFTYPE_WIKI: zTagPrefix = "wiki-"; break; + case CFTYPE_TICKET: zTagPrefix = "tkt-"; break; + case CFTYPE_EVENT: zTagPrefix = "event-"; break; + default: break; + } + if(zTagPrefix!=0){ + nTagOffset = (int)strlen(zTagPrefix); + } + } + db_prepare(&q, + "SELECT tagname, value FROM tagxref, tag" + " WHERE tagxref.rid=%d AND tagxref.tagid=tag.tagid" + " AND tagtype%s%d" + " ORDER BY tagname COLLATE uintnocase", + rid, + zTagType!=0 ? (fInverse!=0?"<>":"=") : ">"/*safe-for-%s*/, + nTagType + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q, 0); + const char *zValue = db_column_text(&q, 1); + if( zTagPrefix && strncmp(zName, zTagPrefix, nTagOffset)==0 ){ + zName += nTagOffset; + } + if( zValue && zValue[0] ){ + fossil_print("%s=%s\n", zName, zValue); + }else{ + fossil_print("%s\n", zName); + } + } + db_finalize(&q); + }else{ + usage("list ?OPTIONS? ?CHECK-IN?"); + } + }else + { + goto tag_cmd_usage; + } + + /* Cleanup */ + return; + +tag_cmd_usage: + usage("add|cancel|find|list ..."); +} + +/* +** COMMAND: reparent* +** +** Usage: %fossil reparent [OPTIONS] CHECK-IN PARENT ... +** +** Create a "parent" tag that causes CHECK-IN to be interpreted as a +** child of PARENT. If multiple PARENTs are listed, then the first is +** the primary parent and others are merge ancestors. +** +** This is an experts-only command. It is used to patch up a repository +** that has been damaged by a shun or that has been pieced together from +** two or more separate repositories. You should never need to reparent +** during normal operations. +** +** Reparenting is accomplished by adding a parent tag. So to undo the +** reparenting operation, simply delete the tag. +** +** --test Make database entries but do not add the tag artifact. +** So the reparent operation will be undone by the next +** "fossil rebuild" command. +** -n|--dry-run Print the tag that would have been created but do not +** actually change the database in any way. +** --date-override DATETIME Set the change time on the control artifact +** --user-override USER Set the user name on the control artifact +*/ +void reparent_cmd(void){ + int bTest = find_option("test","",0)!=0; + int rid; + int i; + Blob value; + char *zUuid; + int dryRun = 0; + const char *zDateOvrd; /* The change time on the control artifact */ + const char *zUserOvrd; /* The user name on the control artifact */ + + if( find_option("dry-run","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN; + zDateOvrd = find_option("date-override",0,1); + zUserOvrd = find_option("user-override",0,1); + db_find_and_open_repository(0, 0); + verify_all_options(); + if( g.argc<4 ){ + usage("[OPTIONS] CHECK-IN PARENT ..."); + } + rid = name_to_typed_rid(g.argv[2], "ci"); + blob_init(&value, 0, 0); + for(i=3; i3 ) blob_append(&value, " ", 1); + zUuid = rid_to_uuid(pid); + blob_append(&value, zUuid, strlen(zUuid)); + fossil_free(zUuid); + } + if( bTest && !dryRun ){ + tag_insert("parent", 1, blob_str(&value), -1, 0.0, rid); + }else{ + zUuid = rid_to_uuid(rid); + tag_add_artifact("","parent",zUuid,blob_str(&value),1|dryRun, + zDateOvrd,zUserOvrd); + } +} + + +/* +** WEBPAGE: taglist +** +** List all non-propagating symbolic tags. +*/ +void taglist_page(void){ + Stmt q; + + login_check_credentials(); + if( !g.perm.Read ){ + login_needed(g.anon.Read); + } + cgi_check_for_malice(); + login_anonymous_available(); + style_header("Tags"); + style_adunit_config(ADUNIT_RIGHT_OK); + style_submenu_element("Timeline", "tagtimeline"); + @

Non-propagating tags:

+ @ + @ + @ + @ + @ + @ + + db_prepare(&q, + "SELECT substr(tagname,5),\n" + "row_number()OVER(ORDER BY tagname COLLATE uintnocase),\n" + "substr(datetime(max(event.mtime)),1,16),\n" + "count(*)\n" + "FROM tagxref JOIN tag USING(tagid)\n" + " JOIN event ON event.objid=tagxref.rid\n" + "WHERE tagname like 'sym-%%'\n" + "AND tagxref.tagtype=1\n" + "GROUP BY 1\n" + "ORDER BY 3 DESC;\n" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q, 0); + int rn = db_column_int(&q, 1); + const char *zDate = db_column_text(&q, 2); + int cnt = db_column_int(&q, 3); + @ \ + }else{ + @ %h(zName)\ + } + @ \ + @ + } + @
Tag NameMost RecentCount
\ + if( g.perm.Hyperlink ){ + @ %z(chref("taglink","%R/timeline?t=%T",zName))%h(zName)   %h(zDate)   %d(cnt)
+ db_finalize(&q); + style_table_sorter(); + style_finish_page(); +} + +/* +** WEBPAGE: /tagtimeline +** +** Render a timeline with all check-ins that contain non-propagating +** symbolic tags. +** +** Query parameters: +** +** ng No graph +** nohidden Hide check-ins with "hidden" tag +** onlyhidden Show only check-ins with "hidden" tag +** brbg Background color by branch name +** ubg Background color by user name +*/ +void tagtimeline_page(void){ + Blob sql = empty_blob; + Stmt q; + int tmFlags; /* Timeline display flags */ + int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */ + int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */ + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + + style_header("Tagged Check-ins"); + style_submenu_element("List", "taglist"); + login_anonymous_available(); + timeline_ss_submenu(); + @

Check-ins with non-propagating tags:

+ blob_append(&sql, timeline_query_for_www(), -1); + blob_append_sql(&sql, + "AND blob.rid IN (SELECT rid FROM tagxref" + " WHERE tagtype=1 AND srcid>0" + " AND tagid IN (SELECT tagid FROM tag " + " WHERE tagname GLOB 'sym-*'))"); + if( fNoHidden || fOnlyHidden ){ + const char* zUnaryOp = fNoHidden ? "NOT" : ""; + blob_append_sql(&sql, + " AND %s EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", + zUnaryOp/*safe-for-%s*/, TAG_HIDDEN); + } + db_prepare(&q, "%s ORDER BY event.mtime DESC /*sort*/", blob_sql_text(&sql)); + blob_reset(&sql); + /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too + ** many descenders to (off-screen) parents. */ + tmFlags = TIMELINE_XMERGE | TIMELINE_FILLGAPS | TIMELINE_NOSCROLL; + if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH; + if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR; + if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR; + www_print_timeline(&q, tmFlags, 0); + db_finalize(&q); + @
+ style_finish_page(); +} + +/* +** Returns true if the given blob.rid value has the given tag ID +** applied to it, else false. +*/ +int rid_has_tag(int rid, int tagId){ + return db_exists( + "SELECT tag.tagid FROM tagxref, tag" + " WHERE tagxref.rid=%d AND tagtype>0 " + " AND tag.tagid=%d" + " AND tagxref.tagid=tag.tagid", + rid, tagId + ); +} + + +/* +** If the given blob.rid value has the given tag applied to it, +** returns true and sets *pOut to a copy of its value (or NULL if it +** has no value). Else returns false and sets *pOut to 0. A truthy +** value returned is the associated tag.tagid value. +** +** Ownership of *pOut is transfered to the caller, who must eventually +** fossil_free() it. +*/ +int rid_has_tag2(int rid, const char *zTag, char **pOut){ + static Stmt q; + int rc = 0; + if( !q.pStmt ){ + db_prepare( + &q, "SELECT t.tagid, x.value" + " FROM tagxref x, tag t" + " WHERE x.rid=:rid" + " AND x.tagtype>0" + " AND x.tagid=t.tagid" + " AND t.tagname=:name" + " ORDER BY mtime DESC" + ); + } + *pOut = 0; + db_bind_int(&q, ":rid", rid); + db_bind_text(&q, ":name", zTag); + if( SQLITE_ROW==db_step(&q) ){ + rc = db_column_int(&q, 0); + *pOut = fossil_strdup(db_column_text(&q, 1)); + } + db_reset(&q); + return rc; +} + + +/* +** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry +** of an active (non-cancelled) tag matching the given rid and tag +** name string, else returns 0. This function does not distinguish +** between a non-existent tag and a cancelled tag. +** +** Design note: the return value is the tagxref.rowid because that +** gives us an easy way to fetch the value of the tag later on, if +** needed. +*/ +int rid_has_active_tag_name(int rid, const char *zTagName){ + static Stmt q = empty_Stmt_m; + int rc; + + assert( 0 != zTagName ); + if( !q.pStmt ){ + db_static_prepare(&q, + "SELECT x.rowid FROM tagxref x, tag t" + " WHERE x.rid=$rid AND x.tagtype>0 " + " AND x.tagid=t.tagid" + " AND t.tagname=$tagname" + ); + } + db_bind_int(&q, "$rid", rid); + db_bind_text(&q, "$tagname", zTagName); + rc = (SQLITE_ROW==db_step(&q)) ? db_column_int(&q, 0) : 0; + db_reset(&q); + return rc; +} -- cgit v1.3.1