Check-in [3b5514ed82]
Not logged in
Overview

SHA1 Hash:3b5514ed825c5cd629359a712b357254a81fe719
Date: 2007-09-22 15:50:14
User: drh
Comment:The "tag" command allows tag artifacts to be inserted for creating and cancelling tags and properties. Timeline responds to bgcolor, br-bgcolor, comment, and user properties.
Timelines: ancestors | descendants | both | trunk
Other Links: files | ZIP archive | manifest

Tags And Properties
Changes
[hide diffs]

Modified src/info.c from [1d02bccab4] to [ae734c2dfe].

@@ -114,11 +114,12 @@
 static int showDescendents(int pid, int depth, const char *zTitle){
   Stmt q;
   int cnt = 0;
   db_prepare(&q,
     "SELECT plink.cid, blob.uuid, datetime(plink.mtime, 'localtime'),"
-    "       event.user, event.comment"
+    "       coalesce(event.euser,event.user),"
+    "       coalesce(event.comment,event.ecomment)"
     "  FROM plink, blob, event"
     " WHERE plink.pid=%d"
     "   AND blob.rid=plink.cid"
     "   AND event.objid=plink.cid"
     " ORDER BY plink.mtime ASC",
@@ -149,10 +150,11 @@
     if( n==0 ){
       db_multi_exec("DELETE FROM leaves WHERE rid=%d", cid);
       @ <b>leaf</b>
     }
   }
+  db_finalize(&q);
   if( cnt ){
     @ </ul>
   }
   return cnt;
 }
@@ -164,11 +166,12 @@
 static void showAncestors(int pid, int depth, const char *zTitle){
   Stmt q;
   int cnt = 0;
   db_prepare(&q,
     "SELECT plink.pid, blob.uuid, datetime(event.mtime, 'localtime'),"
-    "       event.user, event.comment"
+    "       coalesce(event.euser,event.user),"
+    "       coalesce(event.comment,event.ecomment)"
     "  FROM plink, blob, event"
     " WHERE plink.cid=%d"
     "   AND blob.rid=plink.pid"
     "   AND event.objid=plink.pid"
     " ORDER BY event.mtime DESC",
@@ -192,10 +195,11 @@
     @ %s(zCom) (by %s(zUser) on %s(zDate))
     if( depth ){
       showAncestors(cid, depth-1, 0);
     }
   }
+  db_finalize(&q);
   if( cnt ){
     @ </ul>
   }
 }
 
@@ -206,11 +210,12 @@
 static void showLeaves(void){
   Stmt q;
   int cnt = 0;
   db_prepare(&q,
     "SELECT blob.uuid, datetime(event.mtime, 'localtime'),"
-    "       event.user, event.comment"
+    "       coalesce(event.euser, event.user),"
+    "       coalesce(event.ecomment,event.comment)"
     "  FROM leaves, plink, blob, event"
     " WHERE plink.cid=leaves.rid"
     "   AND blob.rid=leaves.rid"
     "   AND event.objid=leaves.rid"
     " ORDER BY event.mtime DESC"
@@ -227,10 +232,61 @@
     }
     @ <li>
     hyperlink_to_uuid(zUuid);
     @ %s(zCom) (by %s(zUser) on %s(zDate))
   }
+  db_finalize(&q);
+  if( cnt ){
+    @ </ul>
+  }
+}
+
+/*
+** Show information about all tags on a given node.
+*/
+static void showTags(int rid){
+  Stmt q;
+  int cnt = 0;
+  db_prepare(&q,
+    "SELECT tag.tagid, tagname, srcid, blob.uuid, value,"
+    "       datetime(tagxref.mtime,'localtime'), addflag"
+    "  FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid"
+    "       LEFT JOIN blob ON blob.rid=tagxref.srcid"
+    " WHERE tagxref.rid=%d"
+    " ORDER BY tagname", rid
+  );
+  while( db_step(&q)==SQLITE_ROW ){
+    int tagid = db_column_int(&q, 0);
+    const char *zTagname = db_column_text(&q, 1);
+    int srcid = db_column_int(&q, 2);
+    const char *zUuid = db_column_text(&q, 3);
+    const char *zValue = db_column_text(&q, 4);
+    const char *zDate = db_column_text(&q, 5);
+    int addFlag = db_column_int(&q, 6);
+    cnt++;
+    if( cnt==1 ){
+      @ <h2>Tags And Properties</h2>
+      @ <ul>
+    }
+    @ <li>
+    @ <b>%h(zTagname)</b>
+    if( zValue ){
+      @ = %h(zValue)<i>
+    }else if( !addFlag ){
+      @ <i>Cancelled
+    }else{
+      @ <i>
+    }
+    if( srcid==0 ){
+      @ Inherited
+    }else if( zUuid ){
+      @ From
+      hyperlink_to_uuid(zUuid);
+    }
+    @ on %s(zDate)</i>
+  }
+  db_finalize(&q);
   if( cnt ){
     @ </ul>
   }
 }
 
@@ -266,21 +322,22 @@
   if( db_step(&q)==SQLITE_ROW ){
     const char *zUuid = db_column_text(&q, 0);
     @ <h2>Version %s(zUuid)</h2>
     @ <ul>
     @ <li><b>Date:</b> %s(db_column_text(&q, 1))</li>
-    @ <li><b>User:</b> %s(db_column_text(&q, 2))</li>
-    @ <li><b>Comment:</b> %s(db_column_text(&q, 3))</li>
+    @ <li><b>Original&nbsp;User:</b> %s(db_column_text(&q, 2))</li>
+    @ <li><b>Original&nbsp;Comment:</b> %s(db_column_text(&q, 3))</li>
     @ <li><a href="%s(g.zBaseURL)/vdiff/%d(rid)">diff</a></li>
     @ <li><a href="%s(g.zBaseURL)/zip/%s(zUuid).zip">ZIP archive</a></li>
     @ <li><a href="%s(g.zBaseURL)/fview/%d(rid)">manifest</a></li>
     if( g.okSetup ){
       @ <li><b>Record ID:</b> %d(rid)</li>
     }
     @ </ul>
   }
   db_finalize(&q);
+  showTags(rid);
   @ <p><h2>Changes:</h2>
   @ <ul>
   db_prepare(&q,
      "SELECT name, pid, fid"
      "  FROM mlink, filename"
@@ -324,11 +381,13 @@
   style_header("File History");
 
   zPrevDate[0] = 0;
   db_prepare(&q,
     "SELECT a.uuid, substr(b.uuid,1,10), datetime(event.mtime,'localtime'),"
-    "       event.comment, event.user, mlink.pid, mlink.fid"
+    "       coalesce(event.ecomment, event.comment),"
+    "       coalesce(event.euser, event.user),"
+    "       mlink.pid, mlink.fid"
     "  FROM mlink, blob a, blob b, event"
     " WHERE mlink.fnid=(SELECT fnid FROM filename WHERE name=%Q)"
     "   AND a.rid=mlink.mid"
     "   AND b.rid=mlink.fid"
     "   AND event.objid=mlink.mid"
@@ -449,11 +508,13 @@
 static void object_description(int rid, int linkToView){
   Stmt q;
   int cnt = 0;
   db_prepare(&q,
     "SELECT filename.name, datetime(event.mtime), substr(a.uuid,1,10),"
-    "       event.comment, event.user, b.uuid"
+    "       coalesce(event.comment,event.ecomment),"
+    "       coalesce(event.euser,event.user),"
+    "       b.uuid"
     "  FROM mlink, filename, event, blob a, blob b"
     " WHERE filename.fnid=mlink.fnid"
     "   AND event.objid=mlink.mid"
     "   AND a.rid=mlink.fid"
     "   AND b.rid=mlink.mid"

Modified src/manifest.c from [f007901442] to [d1a0570f09].

@@ -19,22 +19,31 @@
 **   drh@hwaci.com
 **   http://www.hwaci.com/drh/
 **
 *******************************************************************************
 **
-** This file contains code used to cross link manifests
+** This file contains code used to cross link control files and
+** manifests.
 */
 #include "config.h"
 #include "manifest.h"
 #include <assert.h>
 
 #if INTERFACE
 /*
+** Types of control files
+*/
+#define CFTYPE_MANIFEST   1
+#define CFTYPE_CLUSTER    2
+#define CFTYPE_CONTROL    3
+
+/*
 ** A parsed manifest or cluster.
 */
 struct Manifest {
   Blob content;         /* The original content blob */
+  int type;             /* Type of file */
   char *zComment;       /* Decoded comment */
   double rDate;         /* Time in the "D" line */
   char *zUser;          /* Name of the user */
   char *zRepoCksum;     /* MD5 checksum of the baseline content */
   int nFile;            /* Number of F lines */
@@ -244,10 +253,14 @@
         if( !validate16(zUuid, UUID_SIZE) ) goto manifest_syntax_error;
         defossilize(zName);
         if( zName[0]!='-' && zName[0]!='+' ){
           goto manifest_syntax_error;
         }
+        if( validate16(&zName[1], strlen(&zName[1])) ){
+          /* Do not allow tags whose names look like UUIDs */
+          goto manifest_syntax_error;
+        }
         if( p->nTag>=p->nTagAlloc ){
           p->nTagAlloc = p->nTagAlloc*2 + 10;
           p->aTag = realloc(p->aTag, p->nTagAlloc*sizeof(p->aTag[0]) );
           if( p->aTag==0 ) fossil_panic("out of memory");
         }
@@ -347,21 +360,24 @@
   if( !seenHeader ) goto manifest_syntax_error;
 
   if( p->nFile>0 ){
     if( p->nCChild>0 ) goto manifest_syntax_error;
     if( p->rDate==0.0 ) goto manifest_syntax_error;
+    p->type = CFTYPE_MANIFEST;
   }else if( p->nCChild>0 ){
     if( p->rDate>0.0 ) goto manifest_syntax_error;
     if( p->zComment!=0 ) goto manifest_syntax_error;
     if( p->zUser!=0 ) goto manifest_syntax_error;
     if( p->nTag>0 ) goto manifest_syntax_error;
     if( p->nParent>0 ) goto manifest_syntax_error;
     if( p->zRepoCksum!=0 ) goto manifest_syntax_error;
+    p->type = CFTYPE_CLUSTER;
   }else if( p->nTag>0 ){
     if( p->rDate<=0.0 ) goto manifest_syntax_error;
     if( p->zRepoCksum!=0 ) goto manifest_syntax_error;
     if( p->nParent>0 ) goto manifest_syntax_error;
+    p->type = CFTYPE_CONTROL;
   }else{
     goto manifest_syntax_error;
   }
 
   md5sum_init();
@@ -481,37 +497,49 @@
 
   if( manifest_parse(&m, pContent)==0 ){
     return 0;
   }
   db_begin_transaction();
-  if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
-    for(i=0; i<m.nParent; i++){
-      int pid = uuid_to_rid(m.azParent[i], 1);
-      db_multi_exec("INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime)"
-                    "VALUES(%d, %d, %d, %.17g)", pid, rid, i==0, m.rDate);
-      if( i==0 ){
-        add_mlink(pid, 0, rid, &m);
+  if( m.type==CFTYPE_MANIFEST ){
+    if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
+      for(i=0; i<m.nParent; i++){
+        int pid = uuid_to_rid(m.azParent[i], 1);
+        db_multi_exec("INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime)"
+                      "VALUES(%d, %d, %d, %.17g)", pid, rid, i==0, m.rDate);
+        if( i==0 ){
+          add_mlink(pid, 0, rid, &m);
+        }
+      }
+      db_prepare(&q, "SELECT cid FROM plink WHERE pid=%d AND isprim", rid);
+      while( db_step(&q)==SQLITE_ROW ){
+        int cid = db_column_int(&q, 0);
+        add_mlink(rid, &m, cid, 0);
+      }
+      db_finalize(&q);
+      db_multi_exec(
+        "INSERT INTO event(type,mtime,objid,user,comment)"
+        "VALUES('ci',%.17g,%d,%Q,%Q)",
+        m.rDate, rid, m.zUser, m.zComment
+      );
+    }
+  }
+  if( m.type==CFTYPE_CLUSTER ){
+    for(i=0; i<m.nCChild; i++){
+      int mid;
+      mid = uuid_to_rid(m.azCChild[i], 1);
+      if( mid>0 ){
+        db_multi_exec("DELETE FROM unclustered WHERE rid=%d", mid);
       }
     }
-    db_prepare(&q, "SELECT cid FROM plink WHERE pid=%d AND isprim", rid);
-    while( db_step(&q)==SQLITE_ROW ){
-      int cid = db_column_int(&q, 0);
-      add_mlink(rid, &m, cid, 0);
-    }
-    db_finalize(&q);
-    db_multi_exec(
-      "INSERT INTO event(type,mtime,objid,user,comment)"
-      "VALUES('ci',%.17g,%d,%Q,%Q)",
-      m.rDate, rid, m.zUser, m.zComment
-    );
   }
-  for(i=0; i<m.nCChild; i++){
-    int rid;
-    rid = uuid_to_rid(m.azCChild[i], 1);
-    if( rid>0 ){
-      db_multi_exec("DELETE FROM unclustered WHERE rid=%d", rid);
+  if( m.type==CFTYPE_CONTROL || m.type==CFTYPE_MANIFEST ){
+    for(i=0; i<m.nTag; i++){
+      int tid;
+      tid = uuid_to_rid(m.aTag[i].zUuid, 1);
+      tag_insert(&m.aTag[i].zName[1], m.aTag[i].zName[0]=='+',
+                 m.aTag[i].zValue, rid, m.rDate, tid);
     }
   }
   db_end_transaction(0);
   manifest_clear(&m);
   return 1;
 }

Modified src/name.c from [c08d513136] to [36506ea789].

@@ -34,19 +34,46 @@
 /*
 ** This routine takes a user-entered UUID which might be in mixed
 ** case and might only be a prefix of the full UUID and converts it
 ** into the full-length UUID in canonical form.
 **
+** If the input is not a UUID or a UUID prefix, then try to resolve
+** the name as a tag.
+**
 ** Return the number of errors.
 */
 int name_to_uuid(Blob *pName, int iErrPriority){
   int rc;
   int sz;
   sz = blob_size(pName);
   if( sz>UUID_SIZE || sz<4 || !validate16(blob_buffer(pName), sz) ){
-    fossil_error(iErrPriority, "not a valid object name: %b", pName);
-    return 1;
+    Stmt q;
+    Blob uuid;
+
+    db_prepare(&q,
+      "SELECT (SELECT uuid FROM blob WHERE rid=objid)"
+      "  FROM tagxref JOIN event ON rid=objid"
+      " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%B)"
+      "   AND addflag"
+      "   AND value IS NULL"
+      " ORDER BY event.mtime DESC",
+      pName
+    );
+    blob_zero(&uuid);
+    if( db_step(&q)==SQLITE_ROW ){
+      db_column_blob(&q, 0, &uuid);
+    }
+    db_finalize(&q);
+    if( blob_size(&uuid)==0 ){
+      fossil_error(iErrPriority, "not a valid object name: %b", pName);
+      blob_reset(&uuid);
+      return 1;
+    }else{
+      blob_reset(pName);
+      *pName = uuid;
+      return 0;
+    }
   }
   blob_materialize(pName);
   canonical16(blob_buffer(pName), sz);
   if( sz==UUID_SIZE ){
     rc = db_int(1, "SELECT 0 FROM blob WHERE uuid=%B", pName);
@@ -120,11 +147,11 @@
       return rid;
     }
   }
   blob_init(&name, zName, -1);
   if( name_to_uuid(&name, 1) ){
-    fossil_panic("%s", g.zErrMsg);
+    fossil_fatal("%s", g.zErrMsg);
   }
   rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%B", &name);
   blob_reset(&name);
   return rid;
 }

Modified src/schema.c from [c58feea55a] to [a15ec88bcc].

@@ -156,11 +156,15 @@
 @ CREATE TABLE event(
 @   type TEXT,                      -- Type of event
 @   mtime DATETIME,                 -- Date and time when the event occurs
 @   objid INTEGER PRIMARY KEY,      -- Associated record ID
 @   uid INTEGER REFERENCES user,    -- User who caused the event
+@   bgcolor TEXT,                   -- Color set by 'bgcolor' property
+@   brbgcolor TEXT,                 -- Color set by 'br-bgcolor' property
+@   euser TEXT,                     -- User set by 'user' property
 @   user TEXT,                      -- Name of the user
+@   ecomment TEXT,                  -- Comment set by 'comment' property
 @   comment TEXT                    -- Comment describing the event
 @ );
 @ CREATE INDEX event_i1 ON event(mtime);
 @
 @ -- A record of phantoms.  A phantom is a record for which we know the
@@ -199,10 +203,14 @@
 @ --
 @ CREATE TABLE tag(
 @   tagid INTEGER PRIMARY KEY,       -- Numeric tag ID
 @   tagname TEXT UNIQUE              -- Tag name.  Prefixed by 'v' or 'b'
 @ );
+@ INSERT INTO tag VALUES(1, 'bgcolor');         -- TAG_BGCOLOR
+@ INSERT INTO tag VALUES(2, 'br-bgcolor');      -- TAG_BR_BGCOLOR
+@ INSERT INTO tag VALUES(3, 'comment');         -- TAG_COMMENT
+@ INSERT INTO tag VALUES(4, 'user');            -- TAG_USER
 @
 @ -- Assignments of tags to baselines.  Note that we allow tags to
 @ -- have values assigned to them.  So we are not really dealing with
 @ -- tags here.  These are really properties.  But we are going to
 @ -- keep calling them tags because in many cases the value is ignored.
@@ -216,10 +224,20 @@
 @   rid INTEGER REFERENCE blob,     -- Baseline that tag added/removed from
 @   UNIQUE(rid, tagid)
 @ );
 @ CREATE INDEX tagxref_i1 ON tagxref(tagid);
 ;
+
+/*
+** Predefined tagid values
+*/
+#if INTERFACE
+# define TAG_BGCOLOR    1
+# define TAG_BR_BGCOLOR 2
+# define TAG_COMMENT    3
+# define TAG_USER       4
+#endif
 
 /*
 ** The schema for the locate FOSSIL database file found at the root
 ** of very check-out.  This database contains the complete state of
 ** the checkout.

Modified src/tag.c from [8fad57b8b1] to [c028c6b677].

@@ -42,11 +42,11 @@
   int addFlag,         /* True to add the tag. False to delete it. */
   const char *zValue,  /* Value of the tag.  Might be NULL */
   double mtime         /* Timestamp on the tag */
 ){
   PQueue queue;
-  Stmt s, ins;
+  Stmt s, ins, eventupdate;
   pqueue_init(&queue);
   pqueue_insert(&queue, pid, 0.0);
   db_prepare(&s,
      "SELECT cid, plink.mtime,"
      "       coalesce(srcid=0 AND tagxref.mtime<:mtime, %d) AS doit"
@@ -61,12 +61,18 @@
        "VALUES(%d,1,0,%Q,:mtime,:rid)",
        tagid, zValue
     );
     db_bind_double(&ins, ":mtime", mtime);
   }else{
+    zValue = 0;
     db_prepare(&ins,
        "DELETE FROM tagxref WHERE tagid=%d AND rid=:rid", tagid
+    );
+  }
+  if( tagid==TAG_BR_BGCOLOR ){
+    db_prepare(&eventupdate,
+      "UPDATE event SET brbgcolor=%Q WHERE objid=:rid", zValue
     );
   }
   while( (pid = pqueue_extract(&queue))!=0 ){
     db_bind_int(&s, ":pid", pid);
     while( db_step(&s)==SQLITE_ROW ){
@@ -76,17 +82,25 @@
         double mtime = db_column_double(&s, 1);
         pqueue_insert(&queue, cid, mtime);
         db_bind_int(&ins, ":rid", cid);
         db_step(&ins);
         db_reset(&ins);
+        if( tagid==TAG_BR_BGCOLOR ){
+          db_bind_int(&eventupdate, ":rid", cid);
+          db_step(&eventupdate);
+          db_reset(&eventupdate);
+        }
       }
     }
     db_reset(&s);
   }
   pqueue_clear(&queue);
   db_finalize(&ins);
   db_finalize(&s);
+  if( tagid==TAG_BR_BGCOLOR ){
+    db_finalize(&eventupdate);
+  }
 }
 
 /*
 ** Propagate all propagatable tags in pid to its children.
 */
@@ -144,12 +158,33 @@
     tagid, addFlag, srcId, zValue, rid
   );
   db_bind_double(&s, ":mtime", mtime);
   db_step(&s);
   db_finalize(&s);
+  if( addFlag==0 ){
+    zValue = 0;
+  }
+  switch( tagid ){
+    case TAG_BGCOLOR: {
+      db_multi_exec("UPDATE event SET bgcolor=%Q WHERE objid=%d", zValue, rid);
+      break;
+    }
+    case TAG_BR_BGCOLOR: {
+      db_multi_exec("UPDATE event SET brbgcolor=%Q WHERE objid=%d", zValue,rid);
+      break;
+    }
+    case TAG_COMMENT: {
+      db_multi_exec("UPDATE event SET ecomment=%Q WHERE objid=%d", zValue, rid);
+      break;
+    }
+    case TAG_USER: {
+      db_multi_exec("UPDATE event SET euser=%Q WHERE objid=%d", zValue, rid);
+      break;
+    }
+  }
   if( strncmp(zTag, "br", 2)==0 ){
-    tag_propagate(rid, tagid, 1, zValue, mtime);
+    tag_propagate(rid, tagid, addFlag, zValue, mtime);
   }
 }
 
 
 /*
@@ -201,6 +236,165 @@
     fossil_fatal("no such object: %s", g.argv[3]);
   }
   db_begin_transaction();
   tag_insert(zTag, 0, 0, -1, 0.0, rid);
   db_end_transaction(0);
+}
+
+/*
+** Add a control record to the repository that either creates
+** or cancels a tag.
+*/
+static void tag_add_artifact(
+  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 addFlag                 /* True to add.  false to cancel */
+){
+  int rid;
+  int nrid;
+  char *zDate;
+  Blob uuid;
+  Blob ctrl;
+  Blob cksum;
+
+  user_select();
+  rid = name_to_rid(zObjName);
+  blob_zero(&uuid);
+  db_blob(&uuid, "SELECT uuid FROM blob WHERE rid=%d", rid);
+  blob_zero(&ctrl);
+
+  if( validate16(zTagname, strlen(zTagname)) ){
+    fossil_fatal("invalid tag name \"%s\" - might be confused with a UUID",
+                 zTagname);
+  }
+  zDate = db_text(0, "SELECT datetime('now')");
+  zDate[10] = 'T';
+  blob_appendf(&ctrl, "D %s\n", zDate);
+  blob_appendf(&ctrl, "T %c%F %b", addFlag ? '+' : '-', zTagname, &uuid);
+  if( addFlag && zValue && zValue[0] ){
+    blob_appendf(&ctrl, " %F\n", zValue);
+  }else{
+    blob_appendf(&ctrl, "\n");
+  }
+  blob_appendf(&ctrl, "U %F\n", g.zLogin);
+  md5sum_blob(&ctrl, &cksum);
+  blob_appendf(&ctrl, "Z %b\n", &cksum);
+  db_begin_transaction();
+  nrid = content_put(&ctrl, 0, 0);
+  manifest_crosslink(nrid, &ctrl);
+  db_end_transaction(0);
+}
+
+/*
+** COMMAND: tag
+** Usage: %fossil tag SUBCOMMAND ...
+**
+** Run various subcommands to control tags and properties
+**
+**     %fossil tag add TAGNAME UUID ?VALUE?
+**
+**         Add a new tag or property to UUID.
+**
+**     %fossil tag delete TAGNAME UUID
+**
+**         Delete the tag TAGNAME from UUID
+**
+**     %fossil tag find TAGNAME
+**
+**         List all baselines that use TAGNAME
+**
+**     %fossil tag list ?UUID?
+**
+**         List all tags, or if UUID is supplied, list
+**         all tags and their values for UUID.
+*/
+void tag_cmd(void){
+  int n;
+  db_find_and_open_repository();
+  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;
+    if( g.argc!=5 && g.argc!=6 ){
+      usage("tag add TAGNAME UUID ?VALUE?");
+    }
+    zValue = g.argc==6 ? g.argv[5] : 0;
+    tag_add_artifact(g.argv[3], g.argv[4], zValue, 1);
+  }else
+
+  if( strncmp(g.argv[2],"delete",n)==0 ){
+    if( g.argc!=5 ){
+      usage("tag delete TAGNAME UUID");
+    }
+    tag_add_artifact(g.argv[3], g.argv[4], 0, 0);
+  }else
+
+  if( strncmp(g.argv[2],"find",n)==0 ){
+    Stmt q;
+    if( g.argc!=4 ){
+      usage("tag find TAGNAME");
+    }
+    db_prepare(&q,
+      "SELECT blob.uuid FROM tagxref, blob"
+      " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
+      "   AND blob.rid=tagxref.rid", g.argv[3]
+    );
+    while( db_step(&q)==SQLITE_ROW ){
+      printf("%s\n", db_column_text(&q, 0));
+    }
+    db_finalize(&q);
+  }else
+
+  if( strncmp(g.argv[2],"list",n)==0 ){
+    Stmt q;
+    if( g.argc==3 ){
+      db_prepare(&q,
+        "SELECT tagname"
+        "  FROM tag"
+        " WHERE EXISTS(SELECT 1 FROM tagxref"
+        "               WHERE tagid=tag.tagid"
+        "                 AND addflag)"
+        " ORDER BY tagname"
+      );
+      while( db_step(&q)==SQLITE_ROW ){
+        printf("%s\n", db_column_text(&q, 0));
+      }
+      db_finalize(&q);
+    }else if( g.argc==4 ){
+      int rid = name_to_rid(g.argv[3]);
+      db_prepare(&q,
+        "SELECT tagname, value"
+        "  FROM tagxref, tag"
+        " WHERE tagxref.rid=%d AND tagxref.tagid=tag.tagid"
+        "   AND addflag"
+        " ORDER BY tagname",
+        rid
+      );
+      while( db_step(&q)==SQLITE_ROW ){
+        const char *zName = db_column_text(&q, 0);
+        const char *zValue = db_column_text(&q, 1);
+        if( zValue ){
+          printf("%s=%s\n", zName, zValue);
+        }else{
+          printf("%s\n", zName);
+        }
+      }
+      db_finalize(&q);
+    }else{
+      usage("tag list ?UUID?");
+    }
+  }else
+  {
+    goto tag_cmd_usage;
+  }
+  return;
+
+tag_cmd_usage:
+  usage("add|delete|find|list ...");
 }

Modified src/timeline.c from [34dd916f04] to [e450db18e8].

@@ -198,28 +198,16 @@
   static const char zBaseSql[] =
     @ SELECT
     @   blob.rid,
     @   uuid,
     @   datetime(event.mtime,'localtime'),
-    @   coalesce((SELECT value FROM tagxref
-    @              WHERE rid=blob.rid
-    @                AND tagid=(SELECT tagid FROM tag WHERE tagname='comment')),
-    @            comment),
-    @   coalesce((SELECT value FROM tagxref
-    @              WHERE rid=blob.rid
-    @                AND tagid=(SELECT tagid FROM tag WHERE tagname='user')),
-    @            user),
+    @   coalesce(ecomment, comment),
+    @   coalesce(euser, user),
     @   (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim=1),
     @   (SELECT count(*) FROM plink WHERE cid=blob.rid),
     @   NOT EXISTS (SELECT 1 FROM plink WHERE pid=blob.rid),
-    @   (SELECT value FROM tagxref
-    @     WHERE rid=blob.rid
-    @       AND tagid=(SELECT tagid FROM tag WHERE tagname='bgcolor')
-    @    UNION ALL
-    @    SELECT value FROM tagxref
-    @     WHERE rid=blob.rid
-    @       AND tagid=(SELECT tagid FROM tag WHERE tagname='br-bgcolor'))
+    @   coalesce(bgcolor, brbgcolor)
     @  FROM event JOIN blob
     @ WHERE blob.rid=event.objid
   ;
   return zBaseSql;
 }
@@ -450,20 +438,11 @@
   static const char zBaseSql[] =
     @ SELECT
     @   blob.rid,
     @   uuid,
     @   datetime(event.mtime,'localtime'),
-    @   coalesce((SELECT value FROM tagxref
-    @             WHERE rid=blob.rid
-    @             AND tagid=(SELECT tagid FROM tag WHERE tagname='comment')),
-    @            comment)
-    @     || ' (by ' ||
-    @     coalesce((SELECT value FROM tagxref
-    @               WHERE rid=blob.rid
-    @               AND tagid=(SELECT tagid FROM tag WHERE tagname='user')),
-    @              user)
-    @     || ')',
+    @   coalesce(ecomment,comment) || ' (by ' || coalesce(euser,user) || ')',
     @   (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim),
     @   (SELECT count(*) FROM plink WHERE cid=blob.rid)
     @ FROM event, blob
     @ WHERE blob.rid=event.objid
   ;