Index: src/tkt.c
==================================================================
--- src/tkt.c
+++ src/tkt.c
@@ -899,10 +899,14 @@
 **
 **     %fossil ticket add FIELD VALUE ?FIELD VALUE .. ? ?-q|--quote?
 **
 **         like set, but create a new ticket with the given values.
 **
+**     %fossil ticket history TICKETUUID
+**
+**         Show the complete change history for the ticket
+**
 ** The values in set|add are not validated against the definitions
 ** given in "Ticket Common Script".
 */
 void ticket_cmd(void){
   int n;
@@ -916,16 +920,16 @@
   if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.zLogin) ){
     fossil_fatal("no such user: %s", g.zLogin);
   }
 
   if( g.argc<3 ){
-    usage("add|fieldlist|set|show");
+    usage("add|fieldlist|set|show|history");
   }else{
     n = strlen(g.argv[2]);
     if( n==1 && g.argv[2][0]=='s' ){
       /* set/show cannot be distinguished, so show the usage */
-      usage("add|fieldlist|set|show");
+      usage("add|fieldlist|set|show|history");
     }else if( strncmp(g.argv[2],"list",n)==0 ){
       if( g.argc==3 ){
         usage("list fields|reports");
       }else{
         n = strlen(g.argv[3]);
@@ -970,19 +974,24 @@
           rptshow( zRep, zSep, zFilterUuid, tktEncoding );
 
         }
       }else{
         /* add a new ticket or update an existing ticket */
-        enum { set,add,err } eCmd = err;
+        enum { set,add,history,err } eCmd = err;
         int i = 0;
         int rid;
         const char *zTktUuid = 0;
         Blob tktchng, cksum;
 
         /* get command type (set/add) and get uuid, if needed for set */
-        if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ){
-          eCmd = set;
+        if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ||
+           strncmp(g.argv[2],"history",n)==0 ){
+          if( strncmp(g.argv[2],"history",n)==0 ){
+            eCmd = history;
+          }else{
+            eCmd = set;
+          }
           if( g.argc==3 ){
             usage("set TICKETUUID");
           }
           zTktUuid = db_text(0, 
             "SELECT tkt_uuid FROM ticket WHERE tkt_uuid GLOB '%s*'", g.argv[3]
@@ -996,13 +1005,90 @@
           i = 3;
           zTktUuid = db_text(0, "SELECT lower(hex(randomblob(20)))");
         }
         /* none of set/add, so show the usage! */
         if( eCmd==err ){
-          usage("add|fieldlist|set|show");
+          usage("add|fieldlist|set|show|history");
         }
-        
+
+        /* we just handle history separately here, does not get out */
+        if( eCmd==history ){
+          Stmt q;
+          char *zTitle;
+          int tagid;
+
+          if ( i != g.argc ){
+            fossil_fatal("no other parameters expected to %s!",g.argv[2]);
+          }
+          tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zTktUuid);
+          if( tagid==0 ){
+            fossil_fatal("no such ticket %h", zTktUuid);
+          }  
+          db_prepare(&q,
+            "SELECT datetime(mtime,'localtime'), objid, uuid, NULL, NULL, NULL"
+            "  FROM event, blob"
+            " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
+            "   AND blob.rid=event.objid"
+            " UNION "
+            "SELECT datetime(mtime,'localtime'), attachid, uuid, src, filename, user"
+            "  FROM attachment, blob"
+            " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
+            "   AND blob.rid=attachid"
+            " ORDER BY 1 DESC",
+            tagid, tagid
+          );
+          while( db_step(&q)==SQLITE_ROW ){
+            Manifest *pTicket;
+            char zShort[12];
+            const char *zDate = db_column_text(&q, 0);
+            int rid = db_column_int(&q, 1);
+            const char *zChngUuid = db_column_text(&q, 2);
+            const char *zFile = db_column_text(&q, 4);
+            memcpy(zShort, zChngUuid, 10);
+            zShort[10] = 0;
+            if( zFile!=0 ){
+              const char *zSrc = db_column_text(&q, 3);
+              const char *zUser = db_column_text(&q, 5);
+              if( zSrc==0 || zSrc[0]==0 ){
+                fossil_print("Delete attachment %h\n", zFile);
+              }else{
+                fossil_print("Add attachment %h\n", zFile);
+              }
+              fossil_print(" by %h on %h\n", zUser, zDate);
+            }else{
+              pTicket = manifest_get(rid, CFTYPE_TICKET);
+              if( pTicket ){
+                int i;
+
+                fossil_print("Ticket Change by %h on %h:\n", pTicket->zUser, zDate);
+                for(i=0; i<pTicket->nField; i++){
+                  Blob val;
+                  const char *z;
+                  z = pTicket->aField[i].zName;
+                  blob_set(&val, pTicket->aField[i].zValue);
+                  if( z[0]=='+' ){
+                    fossil_print("  Append to ");
+		    z++;
+		  }else{
+		    fossil_print("  Change ");
+                  }
+		  fossil_print("%h: ",z);
+		  if( blob_size(&val)>50 || contains_newline(&val)) {
+                    fossil_print("\n    ",blob_str(&val));
+                    comment_print(blob_str(&val),4,79);
+                  }else{
+                    fossil_print("%s\n",blob_str(&val));
+                  }
+                  blob_reset(&val);
+                }
+              }
+              manifest_destroy(pTicket);
+            }
+          }
+          db_finalize(&q);
+          return;
+        }
         /* read all given ticket field/value pairs from command line */
         if( i==g.argc ){
           fossil_fatal("empty %s command aborted!",g.argv[2]);
         }
         getAllTicketFields();