Fossil
Check-in [7c2577bd63]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview

SHA1 Hash:7c2577bd637e91b0d3649f75555644cd8b46189d
Date: 2010-03-18 14:40:46
User: drh
Comment:Merge in all of the latest clear-title changes from the trunk.

Tags And Properties
Changes
[hide diffs]

Changes to COPYRIGHT-GPL2.txt

@@ -1,5 +1,19 @@
+Fossil is licensed under the terms of the GPLv2 shown below.  In
+addition, permission is granted to link Fossil against the OpenSSL
+project's "OpenSSL" library (or with modified versions of that
+library that use the same license), and distribute the linked
+executables.  If you modify Fossil, you may extend the exception
+described in this paragraph to your modifications, or not, at your
+discretion.
+
+The "clear-title" branch of Fossil is available for more liberal
+licenses such as Berkeley-style licenses or the Apache license.
+
+The original text of GPLv2 follows.
+--------------------------------------------------------------------------
+
 		    GNU GENERAL PUBLIC LICENSE
 		       Version 2, June 1991
 
  Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

Added src/attach.c

Changes to src/browse.c

@@ -113,10 +113,11 @@
   char *zPrefix;
   Stmt q;
   const char *zCI = P("ci");
   int rid = 0;
   Blob content;
+  Blob dirname;
   Manifest m;
   const char *zSubdirLink;
 
   login_check_credentials();
   if( !g.okHistory ){ login_needed(); return; }
@@ -133,36 +134,47 @@
       zCI = 0;
     }
   }
 
   /* Compute the title of the page */
+  blob_zero(&dirname);
   if( zD ){
-    Blob title;
-
-    blob_zero(&title);
-    blob_appendf(&title, "Files in directory ");
-    hyperlinked_path(zD, &title);
-    @ <h2>%s(blob_str(&title))
-    blob_reset(&title);
+    blob_append(&dirname, "in directory ", -1);
+    hyperlinked_path(zD, &dirname);
     zPrefix = mprintf("%h/", zD);
   }else{
-    @ <h2>Files in the top-level directory
+    blob_append(&dirname, "in the top-level directory", -1);
     zPrefix = "";
   }
   if( zCI ){
     char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
     char zShort[20];
     memcpy(zShort, zUuid, 10);
     zShort[10] = 0;
-    @ of check-in [<a href="vinfo?name=%T(zUuid)">%s(zShort)</a>]</h2>
+    @ <h2>Files of check-in [<a href="vinfo?name=%T(zUuid)">%s(zShort)</a>]
+    @ %s(blob_str(&dirname))</h2>
     zSubdirLink = mprintf("%s/dir?ci=%s&name=%T", g.zBaseURL, zUuid, zPrefix);
     if( zD ){
       style_submenu_element("Top", "Top", "%s/dir?ci=%s", g.zBaseURL, zUuid);
+      style_submenu_element("All", "All", "%s/dir?name=%t", g.zBaseURL, zD);
+    }else{
+      style_submenu_element("All", "All", "%s/dir", g.zBaseURL);
     }
   }else{
-    @ </h2>
+    @ <h2>The union of all files from all check-ins
+    @ %s(blob_str(&dirname))</h2>
     zSubdirLink = mprintf("%s/dir?name=%T", g.zBaseURL, zPrefix);
+    if( zD ){
+      style_submenu_element("Top", "Top", "%s/dir", g.zBaseURL);
+      style_submenu_element("Tip", "Tip", "%s/dir?name=%t&ci=tip",
+                            g.zBaseURL, zD);
+      style_submenu_element("Trunk", "Trunk", "%s/dir?name=%t&ci=trunk",
+                             g.zBaseURL,zD);
+    }else{
+      style_submenu_element("Tip", "Tip", "%s/dir?ci=tip", g.zBaseURL);
+      style_submenu_element("Trunk", "Trunk", "%s/dir?ci=trunk", g.zBaseURL);
+    }
   }
 
   /* Compute the temporary table "localfiles" containing the names
   ** of all files and subdirectories in the zD[] directory.
   **

Changes to src/checkin.c

@@ -195,12 +195,66 @@
   }
   db_finalize(&q);
 }
 
 /*
+** Construct and return a string which is an SQL expression that will
+** be TRUE if value zVal matches any of the GLOB expressions in the list
+** zGlobList.  For example:
+**
+**    zVal:       "x"
+**    zGlobList:  "*.o,*.obj"
+**
+**    Result:     "(x GLOB '*.o' OR x GLOB '*.obj')"
+**
+** Each element of the GLOB list may optionally be enclosed in either '...'
+** or "...".  This allows commas in the expression.  Whitespace at the
+** beginning and end of each GLOB pattern is ignored, except when enclosed
+** within '...' or "...".
+**
+** This routine makes no effort to free the memory space it uses.
+*/
+char *glob_expr(const char *zVal, const char *zGlobList){
+  Blob expr;
+  char *zSep = "(";
+  int nTerm = 0;
+  int i;
+  int cTerm;
+
+  if( zGlobList==0 || zGlobList[0]==0 ) return "0";
+  blob_zero(&expr);
+  while( zGlobList[0] ){
+    while( isspace(zGlobList[0]) || zGlobList[0]==',' ) zGlobList++;
+    if( zGlobList[0]==0 ) break;
+    if( zGlobList[0]=='\'' || zGlobList[0]=='"' ){
+      cTerm = zGlobList[0];
+      zGlobList++;
+    }else{
+      cTerm = ',';
+    }
+    for(i=0; zGlobList[i] && zGlobList[i]!=cTerm; i++){}
+    if( cTerm==',' ){
+      while( i>0 && isspace(zGlobList[i-1]) ){ i--; }
+    }
+    blob_appendf(&expr, "%s%s GLOB '%.*q'", zSep, zVal, i, zGlobList);
+    zSep = " OR ";
+    if( cTerm!=',' && zGlobList[i] ) i++;
+    zGlobList += i;
+    if( zGlobList[0] ) zGlobList++;
+    nTerm++;
+  }
+  if( nTerm ){
+    blob_appendf(&expr, ")");
+    return blob_str(&expr);
+  }else{
+    return "0";
+  }
+}
+
+/*
 ** COMMAND: extras
-** Usage: %fossil extras ?--dotfiles?
+** Usage: %fossil extras ?--dotfiles? ?--ignore GLOBPATTERN?
 **
 ** Print a list of all files in the source tree that are not part of
 ** the current checkout.  See also the "clean" command.
 **
 ** Files and subdirectories whose names begin with "." are normally
@@ -209,20 +263,28 @@
 void extra_cmd(void){
   Blob path;
   Blob repo;
   Stmt q;
   int n;
+  const char *zIgnoreFlag = find_option("ignore",0,1);
   int allFlag = find_option("dotfiles",0,0)!=0;
+
   db_must_be_within_tree();
   db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)");
   n = strlen(g.zLocalRoot);
   blob_init(&path, g.zLocalRoot, n-1);
+  if( zIgnoreFlag==0 ){
+    zIgnoreFlag = db_get("ignore-glob", 0);
+  }
   vfile_scan(0, &path, blob_size(&path), allFlag);
   db_prepare(&q,
       "SELECT x FROM sfile"
       " WHERE x NOT IN ('manifest','manifest.uuid','_FOSSIL_')"
-      " ORDER BY 1");
+      "   AND NOT %s"
+      " ORDER BY 1",
+      glob_expr("x", zIgnoreFlag)
+  );
   if( file_tree_name(g.zRepositoryName, &repo, 0) ){
     db_multi_exec("DELETE FROM sfile WHERE x=%B", &repo);
   }
   while( db_step(&q)==SQLITE_ROW ){
     printf("%s\n", db_column_text(&q, 0));
@@ -683,11 +745,11 @@
   zDate = db_text(0, "SELECT datetime('%q')", zDateOvrd ? zDateOvrd : "now");
   zDate[10] = 'T';
   blob_appendf(&manifest, "D %s\n", zDate);
   zDate[10] = ' ';
   db_prepare(&q,
-    "SELECT pathname, uuid, origname, blob.rid"
+    "SELECT pathname, uuid, origname, blob.rid, isexe"
     "  FROM vfile JOIN blob ON vfile.mrid=blob.rid"
     " WHERE NOT deleted AND vfile.vid=%d"
     " ORDER BY 1", vid);
   blob_zero(&filename);
   blob_appendf(&filename, "%s", g.zLocalRoot);
@@ -695,13 +757,20 @@
   while( db_step(&q)==SQLITE_ROW ){
     const char *zName = db_column_text(&q, 0);
     const char *zUuid = db_column_text(&q, 1);
     const char *zOrig = db_column_text(&q, 2);
     int frid = db_column_int(&q, 3);
+    int isexe = db_column_int(&q, 4);
     const char *zPerm;
     blob_append(&filename, zName, -1);
-    if( file_isexe(blob_str(&filename)) ){
+#ifndef __MINGW32__
+    /* For unix, extract the "executable" permission bit directly from
+    ** the filesystem.  On windows, the "executable" bit is retained
+    ** unchanged from the original. */
+    isexe = file_isexe(blob_str(&filename));
+#endif
+    if( isexe ){
       zPerm = " x";
     }else{
       zPerm = "";
     }
     blob_resize(&filename, nBasename);

Changes to src/checkout.c

@@ -95,10 +95,18 @@
   vfile_build(vid, &manifest);
   blob_reset(&manifest);
 }
 
 /*
+** Set or clear the vfile.isexe flag for a file.
+*/
+static void set_or_clear_isexe(const char *zFilename, int vid, int onoff){
+  db_multi_exec("UPDATE vfile SET isexe=%d WHERE vid=%d and pathname=%Q",
+                onoff, vid, zFilename);
+}
+
+/*
 ** Read the manifest file given by vid out of the repository
 ** and store it in the root of the local check-out.
 */
 void manifest_to_disk(int vid){
   char *zManFile;
@@ -128,10 +136,11 @@
   for(i=0; i<m.nFile; i++){
     int isExe;
     blob_append(&filename, m.aFile[i].zName, -1);
     isExe = m.aFile[i].zPerm && strstr(m.aFile[i].zPerm, "x");
     file_setexe(blob_str(&filename), isExe);
+    set_or_clear_isexe(m.aFile[i].zName, vid, isExe);
     blob_resize(&filename, baseLen);
   }
   blob_reset(&filename);
   manifest_clear(&m);
 }

Changes to src/db.c

@@ -496,20 +496,28 @@
 ** zDefault instead.
 */
 char *db_text(char *zDefault, const char *zSql, ...){
   va_list ap;
   Stmt s;
-  char *z = zDefault;
+  char *z;
   va_start(ap, zSql);
   db_vprepare(&s, zSql, ap);
   va_end(ap);
   if( db_step(&s)==SQLITE_ROW ){
     z = mprintf("%s", sqlite3_column_text(s.pStmt, 0));
+  }else if( zDefault ){
+    z = mprintf("%s", zDefault);
+  }else{
+    z = 0;
   }
   db_finalize(&s);
   return z;
 }
+
+#ifdef __MINGW32__
+extern char *sqlite3_win32_mbcs_to_utf8(const char*);
+#endif
 
 /*
 ** Initialize a new database file with the given schema.  If anything
 ** goes wrong, call db_err() to exit.
 */
@@ -661,29 +669,43 @@
   db_open_or_attach(zDbName, "localdb");
   g.localOpen = 1;
   db_open_config(0);
   db_open_repository(0);
 
+  /* If the "isexe" column is missing from the vfile table, then
+  ** add it now.   This code added on 2010-03-06.  After all users have
+  ** upgraded, this code can be safely deleted.
+  */
+  rc = sqlite3_prepare(g.db, "SELECT isexe FROM vfile", -1, &pStmt, 0);
+  sqlite3_finalize(pStmt);
+  if( rc==SQLITE_ERROR ){
+    sqlite3_exec(g.db, "ALTER TABLE vfile ADD COLUMN isexe BOOLEAN", 0, 0, 0);
+  }
+
+#if 0
   /* If the "mtime" column is missing from the vfile table, then
   ** add it now.   This code added on 2008-12-06.  After all users have
   ** upgraded, this code can be safely deleted.
   */
   rc = sqlite3_prepare(g.db, "SELECT mtime FROM vfile", -1, &pStmt, 0);
   sqlite3_finalize(pStmt);
   if( rc==SQLITE_ERROR ){
     sqlite3_exec(g.db, "ALTER TABLE vfile ADD COLUMN mtime INTEGER", 0, 0, 0);
   }
+#endif
 
+#if 0
   /* If the "origname" column is missing from the vfile table, then
   ** add it now.   This code added on 2008-11-09.  After all users have
   ** upgraded, this code can be safely deleted.
   */
   rc = sqlite3_prepare(g.db, "SELECT origname FROM vfile", -1, &pStmt, 0);
   sqlite3_finalize(pStmt);
   if( rc==SQLITE_ERROR ){
     sqlite3_exec(g.db, "ALTER TABLE vfile ADD COLUMN origname TEXT", 0, 0, 0);
   }
+#endif
 
   return 1;
 }
 
 /*
@@ -885,11 +907,15 @@
 ** The zInitialDate parameter determines the date of the initial check-in
 ** that is automatically created.  If zInitialDate is 0 then no initial
 ** check-in is created. The makeServerCodes flag determines whether or
 ** not server and project codes are invented for this repository.
 */
-void db_initial_setup (const char *zInitialDate, const char *zDefaultUser, int makeServerCodes){
+void db_initial_setup(
+  const char *zInitialDate,    /* Initial date of repository. (ex: "now") */
+  const char *zDefaultUser,    /* Default user for the repository */
+  int makeServerCodes          /* True to make new server & project codes */
+){
   char *zDate;
   Blob hash;
   Blob manifest;
 
   db_set("content-schema", CONTENT_SCHEMA, 0);
@@ -942,10 +968,11 @@
 ** parameter.
 **
 ** Options:
 **
 **    --admin-user|-A USERNAME
+**    --date-override DATETIME
 **
 */
 void create_repository_cmd(void){
   char *zPassword;
   const char *zDate;          /* Date of the initial check-in */
@@ -1406,10 +1433,14 @@
 **    autosync         If enabled, automatically pull prior to commit
 **                     or update and automatically push after commit or
 **                     tag or branch creation.  If the the value is "pullonly"
 **                     then only pull operations occur automatically.
 **
+**    binary-glob      The VALUE is a comma-separated list of GLOB patterns
+**                     that should be treated as binary files for merging
+**                     purposes.  Example:   *.xml
+**
 **    clearsign        When enabled, fossil will attempt to sign all commits
 **                     with gpg.  When disabled (the default), commits will
 **                     be unsigned.
 **
 **    diff-command     External command to run when performing a diff.
@@ -1423,10 +1454,14 @@
 **    gdiff-command    External command to run when performing a graphical
 **                     diff. If undefined, text diff will be used.
 **
 **    http-port        The TCP/IP port number to use by the "server"
 **                     and "ui" commands.  Default: 8080
+**
+**    ignore-glob      The VALUE is a comma-separated list of GLOB patterns
+**                     specifying files that the "extra" command will ignore.
+**                     Example:  *.o,*.obj,*.exe
 **
 **    localauth        If enabled, require that HTTP connections from
 **                     127.0.0.1 be authenticated by password.  If
 **                     false, all HTTP requests from localhost have
 **                     unrestricted access to the repository.
@@ -1448,15 +1483,17 @@
 **                     and "firefox" on Unix.
 */
 void setting_cmd(void){
   static const char *azName[] = {
     "autosync",
+    "binary-glob",
     "clearsign",
     "diff-command",
     "dont-push",
     "editor",
     "gdiff-command",
+    "ignore-glob",
     "http-port",
     "localauth",
     "mtime-changes",
     "pgp-command",
     "proxy",

Changes to src/diffcmd.c

@@ -49,11 +49,11 @@
 }
 
 /*
 ** This function implements a cross-platform "system()" interface.
 */
-int portable_system(char *zOrigCmd){
+int portable_system(const char *zOrigCmd){
   int rc;
 #ifdef __MINGW32__
   /* On windows, we have to put double-quotes around the entire command.
   ** Who knows why - this is just the way windows works.
   */

Changes to src/file.c

@@ -163,11 +163,11 @@
   }else{
     if( (buf.st_mode & 0111)!=0 ){
       chmod(zFilename, buf.st_mode & ~0111);
     }
   }
-#endif
+#endif /* __MINGW32__ */
 }
 
 /*
 ** Create the directory named in the argument, if it does not already
 ** exist.  If forceFlag is 1, delete any prior non-directory object

Changes to src/finfo.c

@@ -100,36 +100,49 @@
 void finfo_page(void){
   Stmt q;
   const char *zFilename;
   char zPrevDate[20];
   Blob title;
+  GraphContext *pGraph;
 
   login_check_credentials();
   if( !g.okRead ){ login_needed(); return; }
   style_header("File History");
   login_anonymous_available();
 
   zPrevDate[0] = 0;
   zFilename = PD("name","");
   db_prepare(&q,
-    "SELECT substr(b.uuid,1,10), datetime(event.mtime,'localtime'),"
-    "       coalesce(event.ecomment, event.comment),"
-    "       coalesce(event.euser, event.user),"
-    "       mlink.pid, mlink.fid, mlink.mid, mlink.fnid, ci.uuid"
+    "SELECT"
+    " substr(b.uuid,1,10),"
+    " datetime(event.mtime,'localtime'),"
+    " coalesce(event.ecomment, event.comment),"
+    " coalesce(event.euser, event.user),"
+    " mlink.pid,"
+    " mlink.fid,"
+    " mlink.mid,"
+    " mlink.fnid,"
+    " ci.uuid,"
+    " event.bgcolor,"
+    " (SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0"
+                                " AND tagxref.rid=mlink.mid)"
     "  FROM mlink, blob b, event, blob ci"
     " WHERE mlink.fnid=(SELECT fnid FROM filename WHERE name=%Q)"
     "   AND b.rid=mlink.fid"
     "   AND event.objid=mlink.mid"
     "   AND event.objid=ci.rid"
     " ORDER BY event.mtime DESC",
+    TAG_BRANCH,
     zFilename
   );
   blob_zero(&title);
   blob_appendf(&title, "History of ");
   hyperlinked_path(zFilename, &title);
   @ <h2>%b(&title)</h2>
   blob_reset(&title);
+  pGraph = graph_init();
+  @ <div id="canvas" style="position:relative;width:1px;height:1px;"></div>
   @ <table cellspacing=0 border=0 cellpadding=0>
   while( db_step(&q)==SQLITE_ROW ){
     const char *zUuid = db_column_text(&q, 0);
     const char *zDate = db_column_text(&q, 1);
     const char *zCom = db_column_text(&q, 2);
@@ -137,33 +150,46 @@
     int fpid = db_column_int(&q, 4);
     int frid = db_column_int(&q, 5);
     int mid = db_column_int(&q, 6);
     int fnid = db_column_int(&q, 7);
     const char *zCkin = db_column_text(&q,8);
+    const char *zBgClr = db_column_text(&q, 9);
+    const char *zBr = db_column_text(&q, 10);
+    int gidx;
+    char zTime[10];
     char zShort[20];
     char zShortCkin[20];
+    if( zBr==0 ) zBr = "trunk";
+    gidx = graph_add_row(pGraph, frid, fpid>0 ? 1 : 0, &fpid, zBr);
     if( memcmp(zDate, zPrevDate, 10) ){
       sprintf(zPrevDate, "%.10s", zDate);
-      @ <tr><td colspan=3>
-      @   <div class="divider">%s(zPrevDate)</div>
+      @ <tr><td>
+      @   <div class="divider"><nobr>%s(zPrevDate)</nobr></div>
       @ </td></tr>
     }
-    @ <tr><td valign="top">%s(&zDate[11])</td>
-    @ <td width="20"></td>
-    @ <td valign="top" align="left">
+    memcpy(zTime, &zDate[11], 5);
+    zTime[5] = 0;
+    @ <tr><td valign="top" align="right">
+    @ <a href="%s(g.zTop)/timeline?c=%t(zDate)">%s(zTime)</a></td>
+    @ <td width="20" align="left" valign="top"><div id="m%d(gidx)"></div></td>
+    if( zBgClr && zBgClr[0] ){
+      @ <td valign="top" align="left" bgcolor="%h(zBgClr)">
+    }else{
+      @ <td valign="top" align="left">
+    }
     sqlite3_snprintf(sizeof(zShort), zShort, "%.10s", zUuid);
     sqlite3_snprintf(sizeof(zShortCkin), zShortCkin, "%.10s", zCkin);
     if( g.okHistory ){
       @ <a href="%s(g.zTop)/artifact/%s(zUuid)">[%s(zShort)]</a>
     }else{
       @ [%s(zShort)]
     }
     @ part of check-in
     hyperlink_to_uuid(zShortCkin);
-    @ %h(zCom) (By:
-    hyperlink_to_user(zUser, zDate, " on");
-    hyperlink_to_date(zDate, ")");
+    @ %h(zCom) (user:
+    hyperlink_to_user(zUser, zDate, "");
+    @ branch: %h(zBr))
     if( g.okHistory ){
       if( fpid ){
         @ <a href="%s(g.zBaseURL)/fdiff?v1=%d(fpid)&amp;v2=%d(frid)">[diff]</a>
       }
       @ <a href="%s(g.zBaseURL)/annotate?mid=%d(mid)&amp;fnid=%d(fnid)">
@@ -170,8 +196,18 @@
       @ [annotate]</a>
       @ </td>
     }
   }
   db_finalize(&q);
+  if( pGraph ){
+    graph_finish(pGraph, 1);
+    if( pGraph->nErr ){
+      graph_free(pGraph);
+      pGraph = 0;
+    }else{
+      @ <tr><td><td><div style="width:%d(pGraph->mxRail*20+30)px;"></div>
+    }
+  }
   @ </table>
+  timeline_output_graph_javascript(pGraph);
   style_footer();
 }

Changes to src/graph.c

@@ -35,19 +35,19 @@
 /* The graph appears vertically beside a timeline.  Each row in the
 ** timeline corresponds to a row in the graph.
 */
 struct GraphRow {
   int rid;                    /* The rid for the check-in */
-  int isLeaf;                 /* True if the check-in is an open leaf */
   int nParent;                /* Number of parents */
   int aParent[GR_MAX_PARENT]; /* Array of parents.  0 element is primary .*/
   char *zBranch;              /* Branch name */
 
   GraphRow *pNext;            /* Next row down in the list of all rows */
   GraphRow *pPrev;            /* Previous row */
 
   int idx;                    /* Row index.  First is 1.  0 used for "none" */
+  int isLeaf;                 /* True if no direct child nodes */
   int iRail;                  /* Which rail this check-in appears on. 0-based.*/
   int aiRaiser[GR_MAX_RAIL];  /* Raisers from this node to a higher row. */
   int bDescender;             /* Raiser from bottom of graph to here. */
   u32 mergeIn;                /* Merge in from other rails */
   int mergeOut;               /* Merge out to this rail */
@@ -63,11 +63,14 @@
   int mxRail;           /* Number of rails required to render the graph */
   GraphRow *pFirst;     /* First row in the list */
   GraphRow *pLast;      /* Last row in the list */
   int nBranch;          /* Number of distinct branches */
   char **azBranch;      /* Names of the branches */
+  int nRow;                  /* Number of rows */
   int railMap[GR_MAX_RAIL];  /* Rail order mapping */
+  int nHash;                 /* Number of slots in apHash[] */
+  GraphRow **apHash;         /* Hash table of rows */
 };
 
 #endif
 
 /*
@@ -99,11 +102,38 @@
     p->pFirst = pRow->pNext;
     free(pRow);
   }
   for(i=0; i<p->nBranch; i++) free(p->azBranch[i]);
   free(p->azBranch);
+  free(p->apHash);
   free(p);
+}
+
+/*
+** Insert a row into the hash table.  If there is already another
+** row with the same rid, the other row is replaced.
+*/
+static void hashInsert(GraphContext *p, GraphRow *pRow){
+  int h;
+  h = pRow->rid % p->nHash;
+  while( p->apHash[h] && p->apHash[h]->rid!=pRow->rid ){
+    h++;
+    if( h>=p->nHash ) h = 0;
+  }
+  p->apHash[h] = pRow;
+}
+
+/*
+** Look up the row with rid.
+*/
+static GraphRow *hashFind(GraphContext *p, int rid){
+  int h = rid % p->nHash;
+  while( p->apHash[h] && p->apHash[h]->rid!=rid ){
+    h++;
+    if( h>=p->nHash ) h = 0;
+  }
+  return p->apHash[h];
 }
 
 /*
 ** Return the canonical pointer for a given branch name.
 ** Multiple calls to this routine with equivalent strings
@@ -122,34 +152,35 @@
 }
 
 /*
 ** Add a new row t the graph context.  Rows are added from top to bottom.
 */
-void graph_add_row(
+int graph_add_row(
   GraphContext *p,     /* The context to which the row is added */
   int rid,             /* RID for the check-in */
-  int isLeaf,          /* True if the check-in is an leaf */
   int nParent,         /* Number of parents */
   int *aParent,        /* Array of parents */
   const char *zBranch  /* Branch for this check-in */
 ){
   GraphRow *pRow;
 
-  if( p->nErr ) return;
-  if( nParent>GR_MAX_PARENT ){ p->nErr++; return; }
+  if( p->nErr ) return 0;
+  if( nParent>GR_MAX_PARENT ){ p->nErr++; return 0; }
   pRow = (GraphRow*)safeMalloc( sizeof(GraphRow) );
   pRow->rid = rid;
-  pRow->isLeaf = isLeaf;
   pRow->nParent = nParent;
   pRow->zBranch = persistBranchName(p, zBranch);
   memcpy(pRow->aParent, aParent, sizeof(aParent[0])*nParent);
   if( p->pFirst==0 ){
     p->pFirst = pRow;
   }else{
     p->pLast->pNext = pRow;
   }
   p->pLast = pRow;
+  p->nRow++;
+  pRow->idx = p->nRow;
+  return pRow->idx;
 }
 
 /*
 ** Return the index of a rail currently not in use for any row between
 ** top and bottom, inclusive.
@@ -188,51 +219,59 @@
 /*
 ** Compute the complete graph
 */
 void graph_finish(GraphContext *p, int omitDescenders){
   GraphRow *pRow, *pDesc;
-  Bag allRids;
-  Bag notLeaf;
   int i;
-  int nRow;
   u32 mask;
   u32 inUse;
 
   if( p==0 || p->pFirst==0 || p->nErr ) return;
 
   /* Initialize all rows */
-  bag_init(&allRids);
-  bag_init(&notLeaf);
-  nRow = 0;
+  p->nHash = p->nRow*2 + 1;
+  p->apHash = safeMalloc( sizeof(p->apHash[0])*p->nHash );
   for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
     if( pRow->pNext ) pRow->pNext->pPrev = pRow;
-    pRow->idx = ++nRow;
     pRow->iRail = -1;
     pRow->mergeOut = -1;
-    bag_insert(&allRids, pRow->rid);
+    hashInsert(p, pRow);
   }
   p->mxRail = -1;
 
   /* Purge merge-parents that are out-of-graph
   */
   for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
     for(i=1; i<pRow->nParent; i++){
-      if( !bag_find(&allRids, pRow->aParent[i]) ){
+      if( hashFind(p, pRow->aParent[i])==0 ){
         pRow->aParent[i] = pRow->aParent[--pRow->nParent];
         i--;
       }
     }
-    if( pRow->nParent>0 && bag_find(&allRids, pRow->aParent[0]) ){
-      bag_insert(&notLeaf, pRow->aParent[0]);
+  }
+
+  /* Figure out which nodes have no direct children (children on
+  ** the same rail).  Mark such nodes is isLeaf.
+  */
+  memset(p->apHash, 0, sizeof(p->apHash[0])*p->nHash);
+  for(pRow=p->pLast; pRow; pRow=pRow->pPrev) pRow->isLeaf = 1;
+  for(pRow=p->pLast; pRow; pRow=pRow->pPrev){
+    GraphRow *pParent;
+    hashInsert(p, pRow);
+    if( pRow->nParent>0
+     && (pParent = hashFind(p, pRow->aParent[0]))!=0
+     && pRow->zBranch==pParent->zBranch
+    ){
+      pParent->isLeaf = 0;
     }
   }
 
   /* Identify rows where the primary parent is off screen.  Assign
   ** each to a rail and draw descenders to the bottom of the screen.
   */
   for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
-    if( pRow->nParent==0 || !bag_find(&allRids,pRow->aParent[0]) ){
+    if( pRow->nParent==0 || hashFind(p,pRow->aParent[0])==0 ){
       if( omitDescenders ){
         pRow->iRail = findFreeRail(p, pRow->idx, pRow->idx, 0, 0);
       }else{
         pRow->iRail = ++p->mxRail;
       }
@@ -257,11 +296,10 @@
   for(pRow=p->pLast; pRow; pRow=pRow->pPrev){
     int parentRid;
     if( pRow->iRail>=0 ) continue;
     assert( pRow->nParent>0 );
     parentRid = pRow->aParent[0];
-    assert( bag_find(&allRids, parentRid) );
     for(pDesc=pRow->pNext; pDesc && pDesc->rid!=parentRid; pDesc=pDesc->pNext){}
     if( pDesc==0 ){
       /* Time skew */
       pRow->iRail = ++p->mxRail;
       pRow->railInUse = 1<<pRow->iRail;
@@ -272,14 +310,14 @@
     }else{
       pRow->iRail = findFreeRail(p, 0, pDesc->idx, inUse, 0);
     }
     pDesc->aiRaiser[pRow->iRail] = pRow->idx;
     mask = 1<<pRow->iRail;
-    if( bag_find(&notLeaf, pRow->rid) ){
-      inUse |= mask;
+    if( pRow->isLeaf ){
+      inUse &= ~mask;
     }else{
-      inUse &= ~mask;
+      inUse |= mask;
     }
     for(pDesc = pRow; ; pDesc=pDesc->pNext){
       assert( pDesc!=0 );
       pDesc->railInUse |= mask;
       if( pDesc->rid==parentRid ) break;
@@ -309,29 +347,14 @@
       pRow->mergeIn |= 1<<pDesc->mergeOut;
     }
   }
 
   /*
-  ** Sort the rail numbers
+  ** Find the maximum rail number.
   */
-#if 0
-  p->mxRail = -1;
-  mask = 0;
-  for(pRow=p->pLast; pRow; pRow=pRow->pPrev){
-    if( (mask & (1<<pRow->iRail))==0 ){
-      p->railMap[pRow->iRail] = ++p->mxRail;
-      mask |= 1<<pRow->iRail;
-    }
-    if( pRow->mergeOut>=0 && (mask & (1<<pRow->mergeOut))==0 ){
-      p->railMap[pRow->mergeOut] = ++p->mxRail;
-      mask |= 1<<pRow->mergeOut;
-    }
-  }
-#else
   for(i=0; i<GR_MAX_RAIL; i++) p->railMap[i] = i;
   p->mxRail = 0;
   for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
     if( pRow->iRail>p->mxRail ) p->mxRail = pRow->iRail;
     if( pRow->mergeOut>p->mxRail ) p->mxRail = pRow->mergeOut;
   }
-#endif
 }

Changes to src/info.c

@@ -732,10 +732,52 @@
       }
       cnt++;
     }
     db_finalize(&q);
   }
+  db_prepare(&q,
+    "SELECT target, filename, datetime(mtime), user, src"
+    "  FROM attachment"
+    " WHERE src=(SELECT uuid FROM blob WHERE rid=%d)"
+    " ORDER BY mtime DESC",
+    rid
+  );
+  while( db_step(&q)==SQLITE_ROW ){
+    const char *zTarget = db_column_text(&q, 0);
+    const char *zFilename = db_column_text(&q, 1);
+    const char *zDate = db_column_text(&q, 2);
+    const char *zUser = db_column_text(&q, 3);
+    const char *zSrc = db_column_text(&q, 4);
+    if( cnt>0 ){
+      @ Also attachment "%h(zFilename)" to
+    }else{
+      @ Attachment "%h(zFilename)" to
+    }
+    if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){
+      char zShort[20];
+      memcpy(zShort, zTarget, 10);
+      if( g.okHistory && g.okRdTkt ){
+        @ ticket [<a href="%s(g.zTop)/tktview?name=%s(zShort)">%s(zShort)</a>]
+      }else{
+        @ ticket [%s(zShort)]
+      }
+    }else{
+      if( g.okHistory && g.okRdWiki ){
+        @ wiki page [<a href="%s(g.zTop)/wiki?name=%t(zTarget)">%h(zTarget)</a>]
+      }else{
+        @ wiki page [%h(zTarget)]
+      }
+    }
+    @ added by
+    hyperlink_to_user(zUser,zDate," on");
+    hyperlink_to_date(zDate,".");
+    cnt++;
+    if( pDownloadName && blob_size(pDownloadName)==0 ){
+      blob_append(pDownloadName, zSrc, -1);
+    }
+  }
+  db_finalize(&q);
   if( cnt==0 ){
     char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
     @ Control artifact.
     if( pDownloadName && blob_size(pDownloadName)==0 ){
       blob_append(pDownloadName, zUuid, -1);

Changes to src/login.c

@@ -476,11 +476,12 @@
       case 's':   g.okSetup = 1;  /* Fall thru into Admin */
       case 'a':   g.okAdmin = g.okRdTkt = g.okWrTkt = g.okZip =
                               g.okRdWiki = g.okWrWiki = g.okNewWiki =
                               g.okApndWiki = g.okHistory = g.okClone =
                               g.okNewTkt = g.okPassword = g.okRdAddr =
-                              g.okTktFmt = 1;  /* Fall thru into Read/Write */
+                              g.okTktFmt = g.okAttach = 1;
+                              /* Fall thru into Read/Write */
       case 'i':   g.okRead = g.okWrite = 1;                     break;
       case 'o':   g.okRead = 1;                                 break;
       case 'z':   g.okZip = 1;                                  break;
 
       case 'd':   g.okDelete = 1;                               break;
@@ -498,10 +499,11 @@
       case 'n':   g.okNewTkt = 1;                               break;
       case 'w':   g.okWrTkt = g.okRdTkt = g.okNewTkt =
                   g.okApndTkt = 1;                              break;
       case 'c':   g.okApndTkt = 1;                              break;
       case 't':   g.okTktFmt = 1;                               break;
+      case 'b':   g.okAttach = 1;                               break;
 
       /* The "u" privileges is a little different.  It recursively
       ** inherits all privileges of the user named "reader" */
       case 'u': {
         if( zUser==0 ){
@@ -534,11 +536,11 @@
   int rc = 1;
   if( nCap<0 ) nCap = strlen(zCap);
   for(i=0; i<nCap && rc && zCap[i]; i++){
     switch( zCap[i] ){
       case 'a':  rc = g.okAdmin;     break;
-      /* case 'b': */
+      case 'b':  rc = g.okAttach;    break;
       case 'c':  rc = g.okApndTkt;   break;
       case 'd':  rc = g.okDelete;    break;
       case 'e':  rc = g.okRdAddr;    break;
       case 'f':  rc = g.okNewWiki;   break;
       case 'g':  rc = g.okClone;     break;

Changes to src/main.c

@@ -130,10 +130,11 @@
   int okWrWiki;           /* k: edit wiki via web */
   int okRdTkt;            /* r: view tickets via web */
   int okNewTkt;           /* n: create new tickets */
   int okApndTkt;          /* c: append to tickets via the web */
   int okWrTkt;            /* w: make changes to tickets via web */
+  int okAttach;           /* b: add attachments */
   int okTktFmt;           /* t: create new ticket report formats */
   int okRdAddr;           /* e: read email addresses or other private data */
   int okZip;              /* z: download zipped artifact via /zip URL */
 
   /* For defense against Cross-site Request Forgery attacks */

Changes to src/main.mk

@@ -13,10 +13,11 @@
 
 
 SRC = \
   $(SRCDIR)/add.c \
   $(SRCDIR)/allrepo.c \
+  $(SRCDIR)/attach.c \
   $(SRCDIR)/bag.c \
   $(SRCDIR)/blob.c \
   $(SRCDIR)/branch.c \
   $(SRCDIR)/browse.c \
   $(SRCDIR)/captcha.c \
@@ -83,10 +84,11 @@
   $(SRCDIR)/zip.c
 
 TRANS_SRC = \
   add_.c \
   allrepo_.c \
+  attach_.c \
   bag_.c \
   blob_.c \
   branch_.c \
   browse_.c \
   captcha_.c \
@@ -153,10 +155,11 @@
   zip_.c
 
 OBJ = \
  $(OBJDIR)/add.o \
  $(OBJDIR)/allrepo.o \
+ $(OBJDIR)/attach.o \
  $(OBJDIR)/bag.o \
  $(OBJDIR)/blob.o \
  $(OBJDIR)/branch.o \
  $(OBJDIR)/browse.o \
  $(OBJDIR)/captcha.o \
@@ -264,16 +267,16 @@
 	# noop
 
 clean:
 	rm -f $(OBJDIR)/*.o *_.c $(APPNAME) VERSION.h
 	rm -f translate makeheaders mkindex page_index.h headers
-	rm -f add.h allrepo.h bag.h blob.h branch.h browse.h captcha.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h finfo.h graph.h http.h http_socket.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h schema.h search.h setup.h sha1.h shun.h skins.h stat.h style.h sync.h tag.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h
+	rm -f add.h allrepo.h attach.h bag.h blob.h branch.h browse.h captcha.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h finfo.h graph.h http.h http_socket.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h schema.h search.h setup.h sha1.h shun.h skins.h stat.h style.h sync.h tag.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h
 
 page_index.h: $(TRANS_SRC) mkindex
 	./mkindex $(TRANS_SRC) >$@
 headers:	page_index.h makeheaders VERSION.h
-	./makeheaders  add_.c:add.h allrepo_.c:allrepo.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h finfo_.c:finfo.h graph_.c:graph.h http_.c:http.h http_socket_.c:http_socket.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h skins_.c:skins.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h
+	./makeheaders  add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h finfo_.c:finfo.h graph_.c:graph.h http_.c:http.h http_socket_.c:http_socket.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h skins_.c:skins.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h
 	touch headers
 headers: Makefile
 Makefile:
 add_.c:	$(SRCDIR)/add.c translate
 	./translate $(SRCDIR)/add.c >add_.c
@@ -287,10 +290,17 @@
 
 $(OBJDIR)/allrepo.o:	allrepo_.c allrepo.h  $(SRCDIR)/config.h
 	$(XTCC) -o $(OBJDIR)/allrepo.o -c allrepo_.c
 
 allrepo.h:	headers
+attach_.c:	$(SRCDIR)/attach.c translate
+	./translate $(SRCDIR)/attach.c >attach_.c
+
+$(OBJDIR)/attach.o:	attach_.c attach.h  $(SRCDIR)/config.h
+	$(XTCC) -o $(OBJDIR)/attach.o -c attach_.c
+
+attach.h:	headers
 bag_.c:	$(SRCDIR)/bag.c translate
 	./translate $(SRCDIR)/bag.c >bag_.c
 
 $(OBJDIR)/bag.o:	bag_.c bag.h  $(SRCDIR)/config.h
 	$(XTCC) -o $(OBJDIR)/bag.o -c bag_.c

Changes to src/makemake.tcl

@@ -7,10 +7,11 @@
 # "translate" and "makeheaders"
 #
 set src {
   add
   allrepo
+  attach
   bag
   blob
   branch
   browse
   captcha

Changes to src/manifest.c

@@ -37,67 +37,56 @@
 #define CFTYPE_MANIFEST   1
 #define CFTYPE_CLUSTER    2
 #define CFTYPE_CONTROL    3
 #define CFTYPE_WIKI       4
 #define CFTYPE_TICKET     5
-
-/*
-** Mode parameter values
-*/
-#define CFMODE_READ       1
-#define CFMODE_APPEND     2
-#define CFMODE_WRITE      3
+#define CFTYPE_ATTACHMENT 6
 
 /*
 ** A parsed manifest or cluster.
 */
 struct Manifest {
   Blob content;         /* The original content blob */
-  int type;             /* Type of file */
-  int mode;             /* Access mode */
-  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 */
-  char *zWiki;          /* Text of the wiki page */
-  char *zWikiTitle;     /* Name of the wiki page */
-  char *zTicketUuid;    /* UUID for a ticket */
-  int nFile;            /* Number of F lines */
+  int type;             /* Type of artifact.  One of CFTYPE_xxxxx */
+  char *zComment;       /* Decoded comment.  The C card. */
+  double rDate;         /* Date and time from D card.  0.0 if no D card. */
+  char *zUser;          /* Name of the user from the U card. */
+  char *zRepoCksum;     /* MD5 checksum of the baseline content.  R card. */
+  char *zWiki;          /* Text of the wiki page.  W card. */
+  char *zWikiTitle;     /* Name of the wiki page. L card. */
+  char *zTicketUuid;    /* UUID for a ticket. K card. */
+  char *zAttachName;    /* Filename of an attachment. A card. */
+  char *zAttachSrc;     /* UUID of document being attached. A card. */
+  char *zAttachTarget;  /* Ticket or wiki that attachment applies to.  A card */
+  int nFile;            /* Number of F cards */
   int nFileAlloc;       /* Slots allocated in aFile[] */
   struct {
     char *zName;           /* Name of a file */
     char *zUuid;           /* UUID of the file */
     char *zPerm;           /* File permissions */
     char *zPrior;          /* Prior name if the name was changed */
     int iRename;           /* index of renamed name in prior/next manifest */
-  } *aFile;
-  int nParent;          /* Number of parents */
+  } *aFile;             /* One entry for each F card */
+  int nParent;          /* Number of parents. */
   int nParentAlloc;     /* Slots allocated in azParent[] */
-  char **azParent;      /* UUIDs of parents */
+  char **azParent;      /* UUIDs of parents.  One for each P card argument */
   int nCChild;          /* Number of cluster children */
   int nCChildAlloc;     /* Number of closts allocated in azCChild[] */
-  char **azCChild;      /* UUIDs of referenced objects in a cluster */
-  int nTag;             /* Number of T lines */
+  char **azCChild;      /* UUIDs of referenced objects in a cluster. M cards */
+  int nTag;             /* Number of T Cards */
   int nTagAlloc;        /* Slots allocated in aTag[] */
   struct {
     char *zName;           /* Name of the tag */
     char *zUuid;           /* UUID that the tag is applied to */
     char *zValue;          /* Value if the tag is really a property */
-  } *aTag;
-  int nField;           /* Number of J lines */
+  } *aTag;              /* One for each T card */
+  int nField;           /* Number of J cards */
   int nFieldAlloc;      /* Slots allocated in aField[] */
   struct {
     char *zName;           /* Key or field name */
     char *zValue;          /* Value of the field */
-  } *aField;
-  int nAttach;          /* Number of A lines */
-  int nAttachAlloc;     /* Slots allocated in aAttach[] */
-  struct {
-    char *zUuid;           /* UUID of the attachment */
-    char *zName;           /* Name of the attachment */
-    char *zDesc;           /* Description of the attachment */
-  } *aAttach;
+  } *aField;            /* One for each J card */
 };
 #endif
 
 
 /*
@@ -108,11 +97,10 @@
   free(p->aFile);
   free(p->azParent);
   free(p->azCChild);
   free(p->aTag);
   free(p->aField);
-  free(p->aAttach);
   memset(p, 0, sizeof(*p));
 }
 
 /*
 ** Parse a blob into a Manifest object.  The Manifest object
@@ -178,44 +166,43 @@
     cPrevType = z[0];
     seenHeader = 1;
     if( blob_token(&line, &token)!=1 ) goto manifest_syntax_error;
     switch( z[0] ){
       /*
-      **     A <uuid> <filename> <description>
+      **     A <filename> <target> ?<source>?
       **
       ** Identifies an attachment to either a wiki page or a ticket.
-      ** <uuid> is the artifact that is the attachment.
+      ** <source> is the artifact that is the attachment.  <source>
+      ** is omitted to delete an attachment.  <target> is the name of
+      ** a wiki page or ticket to which that attachment is connected.
       */
       case 'A': {
-        char *zName, *zUuid, *zDesc;
+        char *zName, *zTarget, *zSrc;
         md5sum_step_text(blob_buffer(&line), blob_size(&line));
         if( blob_token(&line, &a1)==0 ) goto manifest_syntax_error;
         if( blob_token(&line, &a2)==0 ) goto manifest_syntax_error;
-        if( blob_token(&line, &a3)==0 ) goto manifest_syntax_error;
-        zUuid = blob_terminate(&a1);
-        zName = blob_terminate(&a2);
-        zDesc = blob_terminate(&a3);
-        if( blob_size(&a1)!=UUID_SIZE ) goto manifest_syntax_error;
-        if( !validate16(zUuid, UUID_SIZE) ) goto manifest_syntax_error;
+        if( p->zAttachName!=0 ) goto manifest_syntax_error;
+        zName = blob_terminate(&a1);
+        zTarget = blob_terminate(&a2);
+        blob_token(&line, &a3);
+        zSrc = blob_terminate(&a3);
         defossilize(zName);
         if( !file_is_simple_pathname(zName) ){
           goto manifest_syntax_error;
         }
-        defossilize(zDesc);
-        if( p->nAttach>=p->nAttachAlloc ){
-          p->nAttachAlloc = p->nAttachAlloc*2 + 10;
-          p->aAttach = realloc(p->aAttach,
-                               p->nAttachAlloc*sizeof(p->aAttach[0]) );
-          if( p->aAttach==0 ) fossil_panic("out of memory");
-        }
-        i = p->nAttach++;
-        p->aAttach[i].zUuid = zUuid;
-        p->aAttach[i].zName = zName;
-        p->aAttach[i].zDesc = zDesc;
-        if( i>0 && strcmp(p->aAttach[i-1].zUuid, zUuid)>=0 ){
+        defossilize(zTarget);
+        if( (blob_size(&a2)!=UUID_SIZE || !validate16(zTarget, UUID_SIZE))
+           && !wiki_name_is_wellformed((const unsigned char *)zTarget) ){
           goto manifest_syntax_error;
         }
+        if( blob_size(&a3)>0
+         && (blob_size(&a3)!=UUID_SIZE || !validate16(zSrc, UUID_SIZE)) ){
+          goto manifest_syntax_error;
+        }
+        p->zAttachName = (char*)file_tail(zName);
+        p->zAttachSrc = zSrc;
+        p->zAttachTarget = zTarget;
         break;
       }
 
       /*
       **     C <comment>
@@ -247,33 +234,10 @@
         if( p->rDate!=0.0 ) goto manifest_syntax_error;
         if( blob_token(&line, &a1)==0 ) goto manifest_syntax_error;
         if( blob_token(&line, &a2)!=0 ) goto manifest_syntax_error;
         zDate = blob_terminate(&a1);
         p->rDate = db_double(0.0, "SELECT julianday(%Q)", zDate);
-        break;
-      }
-
-      /*
-      **     E <mode>
-      **
-      ** Access mode.  <mode> can be one of "read", "append",
-      ** or "write".
-      */
-      case 'E': {
-        md5sum_step_text(blob_buffer(&line), blob_size(&line));
-        if( p->mode!=0 ) goto manifest_syntax_error;
-        if( blob_token(&line, &a1)==0 ) goto manifest_syntax_error;
-        if( blob_token(&line, &a2)!=0 ) goto manifest_syntax_error;
-        if( blob_eq(&a1, "write") ){
-          p->mode = CFMODE_WRITE;
-        }else if( blob_eq(&a1, "append") ){
-          p->mode = CFMODE_APPEND;
-        }else if( blob_eq(&a1, "read") ){
-          p->mode = CFMODE_READ;
-        }else{
-          goto manifest_syntax_error;
-        }
         break;
       }
 
       /*
       **     F <filename> <uuid> ?<permissions>? ?<old-name>?
@@ -608,72 +572,75 @@
   if( p->nFile>0 || p->zRepoCksum!=0 ){
     if( p->nCChild>0 ) goto manifest_syntax_error;
     if( p->rDate==0.0 ) goto manifest_syntax_error;
     if( p->nField>0 ) goto manifest_syntax_error;
     if( p->zTicketUuid ) goto manifest_syntax_error;
-    if( p->nAttach>0 ) goto manifest_syntax_error;
     if( p->zWiki ) goto manifest_syntax_error;
     if( p->zWikiTitle ) goto manifest_syntax_error;
     if( p->zTicketUuid ) goto manifest_syntax_error;
+    if( p->zAttachName ) 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;
     if( p->nField>0 ) goto manifest_syntax_error;
     if( p->zTicketUuid ) goto manifest_syntax_error;
-    if( p->nAttach>0 ) goto manifest_syntax_error;
     if( p->zWiki ) goto manifest_syntax_error;
     if( p->zWikiTitle ) goto manifest_syntax_error;
+    if( p->zAttachName ) goto manifest_syntax_error;
     if( !seenZ ) goto manifest_syntax_error;
     p->type = CFTYPE_CLUSTER;
   }else if( p->nField>0 ){
     if( p->rDate==0.0 ) goto manifest_syntax_error;
-    if( p->zRepoCksum!=0 ) goto manifest_syntax_error;
     if( p->zWiki ) goto manifest_syntax_error;
     if( p->zWikiTitle ) goto manifest_syntax_error;
     if( p->nCChild>0 ) goto manifest_syntax_error;
     if( p->nTag>0 ) goto manifest_syntax_error;
     if( p->zTicketUuid==0 ) goto manifest_syntax_error;
     if( p->zUser==0 ) goto manifest_syntax_error;
+    if( p->zAttachName ) goto manifest_syntax_error;
     if( !seenZ ) goto manifest_syntax_error;
     p->type = CFTYPE_TICKET;
   }else if( p->zWiki!=0 ){
     if( p->rDate==0.0 ) goto manifest_syntax_error;
-    if( p->zRepoCksum!=0 ) goto manifest_syntax_error;
     if( p->nCChild>0 ) goto manifest_syntax_error;
     if( p->nTag>0 ) goto manifest_syntax_error;
     if( p->zTicketUuid!=0 ) goto manifest_syntax_error;
     if( p->zWikiTitle==0 ) goto manifest_syntax_error;
+    if( p->zAttachName ) goto manifest_syntax_error;
     if( !seenZ ) goto manifest_syntax_error;
     p->type = CFTYPE_WIKI;
   }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;
-    if( p->nAttach>0 ) goto manifest_syntax_error;
-    if( p->nField>0 ) goto manifest_syntax_error;
-    if( p->zWiki ) goto manifest_syntax_error;
     if( p->zWikiTitle ) goto manifest_syntax_error;
     if( p->zTicketUuid ) goto manifest_syntax_error;
+    if( p->zAttachName ) goto manifest_syntax_error;
     if( !seenZ ) goto manifest_syntax_error;
     p->type = CFTYPE_CONTROL;
-  }else{
+  }else if( p->zAttachName ){
     if( p->nCChild>0 ) goto manifest_syntax_error;
     if( p->rDate==0.0 ) goto manifest_syntax_error;
+    if( p->zTicketUuid ) goto manifest_syntax_error;
+    if( p->zWikiTitle ) goto manifest_syntax_error;
+    if( !seenZ ) goto manifest_syntax_error;
+    p->type = CFTYPE_ATTACHMENT;
+  }else{
+    if( p->nCChild>0 ) goto manifest_syntax_error;
+    if( p->rDate<=0.0 ) goto manifest_syntax_error;
+    if( p->nParent>0 ) goto manifest_syntax_error;
     if( p->nField>0 ) goto manifest_syntax_error;
     if( p->zTicketUuid ) goto manifest_syntax_error;
-    if( p->nAttach>0 ) goto manifest_syntax_error;
     if( p->zWiki ) goto manifest_syntax_error;
     if( p->zWikiTitle ) goto manifest_syntax_error;
     if( p->zTicketUuid ) goto manifest_syntax_error;
+    if( p->zAttachName ) goto manifest_syntax_error;
     p->type = CFTYPE_MANIFEST;
   }
-
   md5sum_init();
   return 1;
 
 manifest_syntax_error:
   /*fprintf(stderr, "Manifest error on line %i\n", lineNo);fflush(stderr);*/
@@ -1032,10 +999,11 @@
     return 0;
   }
   db_begin_transaction();
   if( m.type==CFTYPE_MANIFEST ){
     if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
+      char *zCom;
       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 ){
@@ -1065,10 +1033,14 @@
         rid, m.zUser, m.zComment,
         TAG_BGCOLOR, rid,
         TAG_USER, rid,
         TAG_COMMENT, rid
       );
+      zCom = db_text(0, "SELECT coalesce(ecomment, comment) FROM event"
+                        " WHERE rowid=last_insert_rowid()");
+      wiki_extract_links(zCom, rid, 0, m.rDate, 1, WIKI_INLINE);
+      free(zCom);
     }
   }
   if( m.type==CFTYPE_CLUSTER ){
     tag_insert("cluster", 1, 0, rid, m.rDate, rid);
     for(i=0; i<m.nCChild; i++){
@@ -1108,11 +1080,16 @@
   if( m.type==CFTYPE_WIKI ){
     char *zTag = mprintf("wiki-%s", m.zWikiTitle);
     int tagid = tag_findid(zTag, 1);
     int prior;
     char *zComment;
-    tag_insert(zTag, 1, 0, rid, m.rDate, rid);
+    int nWiki;
+    char zLength[40];
+    while( isspace(m.zWiki[0]) ) m.zWiki++;
+    nWiki = strlen(m.zWiki);
+    sqlite3_snprintf(sizeof(zLength), zLength, "%d", nWiki);
+    tag_insert(zTag, 1, zLength, rid, m.rDate, rid);
     free(zTag);
     prior = db_int(0,
       "SELECT rid FROM tagxref"
       " WHERE tagid=%d AND mtime<%.17g"
       " ORDER BY mtime DESC",
@@ -1119,11 +1096,15 @@
       tagid, m.rDate
     );
     if( prior ){
       content_deltify(prior, rid, 0);
     }
-    zComment = mprintf("Changes to wiki page [%h]", m.zWikiTitle);
+    if( nWiki>0 ){
+      zComment = mprintf("Changes to wiki page [%h]", m.zWikiTitle);
+    }else{
+      zComment = mprintf("Deleted wiki page [%h]", m.zWikiTitle);
+    }
     db_multi_exec(
       "REPLACE INTO event(type,mtime,objid,user,comment,"
       "                  bgcolor,euser,ecomment)"
       "VALUES('w',%.17g,%d,%Q,%Q,"
       "  (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>1),"
@@ -1144,10 +1125,60 @@
     zTag = mprintf("tkt-%s", m.zTicketUuid);
     tag_insert(zTag, 1, 0, rid, m.rDate, rid);
     free(zTag);
     db_multi_exec("INSERT OR IGNORE INTO pending_tkt VALUES(%Q)",
                   m.zTicketUuid);
+  }
+  if( m.type==CFTYPE_ATTACHMENT ){
+    db_multi_exec(
+       "INSERT INTO attachment(attachid, mtime, src, target,"
+                                        "filename, comment, user)"
+       "VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);",
+       rid, m.rDate, m.zAttachSrc, m.zAttachTarget, m.zAttachName,
+       (m.zComment ? m.zComment : ""), m.zUser
+    );
+    db_multi_exec(
+       "UPDATE attachment SET isLatest = (mtime=="
+          "(SELECT max(mtime) FROM attachment"
+          "  WHERE target=%Q AND filename=%Q))"
+       " WHERE target=%Q AND filename=%Q",
+       m.zAttachTarget, m.zAttachName,
+       m.zAttachTarget, m.zAttachName
+    );
+    if( strlen(m.zAttachTarget)!=UUID_SIZE
+     || !validate16(m.zAttachTarget, UUID_SIZE)
+    ){
+      char *zComment;
+      if( m.zAttachSrc && m.zAttachSrc[0] ){
+        zComment = mprintf("Add attachment \"%h\" to wiki page [%h]",
+             m.zAttachName, m.zAttachTarget);
+      }else{
+        zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]",
+             m.zAttachName, m.zAttachTarget);
+      }
+      db_multi_exec(
+        "REPLACE INTO event(type,mtime,objid,user,comment)"
+        "VALUES('w',%.17g,%d,%Q,%Q)",
+        m.rDate, rid, m.zUser, zComment
+      );
+      free(zComment);
+    }else{
+      char *zComment;
+      if( m.zAttachSrc && m.zAttachSrc[0] ){
+        zComment = mprintf("Add attachment \"%h\" to ticket [%.10s]",
+             m.zAttachName, m.zAttachTarget);
+      }else{
+        zComment = mprintf("Delete attachment \"%h\" from ticket [%.10s]",
+             m.zAttachName, m.zAttachTarget);
+      }
+      db_multi_exec(
+        "REPLACE INTO event(type,mtime,objid,user,comment)"
+        "VALUES('t',%.17g,%d,%Q,%Q)",
+        m.rDate, rid, m.zUser, zComment
+      );
+      free(zComment);
+    }
   }
   db_end_transaction(0);
   manifest_clear(&m);
   return 1;
 }

Changes to src/merge.c

@@ -30,36 +30,51 @@
 
 
 /*
 ** COMMAND: merge
 **
-** Usage: %fossil merge [--cherrypick] VERSION
+** Usage: %fossil merge [--cherrypick] [--backout] VERSION
 **
 ** The argument is a version that should be merged into the current
 ** checkout.  All changes from VERSION back to the nearest common
-** ancestor are merged.  Except, if the --cherrypick option is used
-** only the changes associated with the single check-in VERSION are
-** merged.
+** ancestor are merged.  Except, if either of the --cherrypick or
+** --backout options are used only the changes associated with the
+** single check-in VERSION are merged.  The --backout option causes
+** the changes associated with VERSION to be removed from the current
+** checkout rather than added.
 **
 ** Only file content is merged.  The result continues to use the
-** file and directory names from the current check-out even if those
+** file and directory names from the current checkout even if those
 ** names might have been changed in the branch being merged in.
+**
+** Other options:
+**
+**   --detail                Show additional details of the merge
+**
+**   --binary GLOBPATTERN    Treat files that match GLOBPATTERN as binary
+**                           and do not try to merge parallel changes.  This
+**                           option overrides the "binary-glob" setting.
 */
 void merge_cmd(void){
   int vid;              /* Current version */
   int mid;              /* Version we are merging against */
   int pid;              /* The pivot version - most recent common ancestor */
   int detailFlag;       /* True if the --detail option is present */
   int pickFlag;         /* True if the --cherrypick option is present */
+  int backoutFlag;      /* True if the --backout optioni is present */
+  const char *zBinGlob; /* The value of --binary */
   Stmt q;
 
   detailFlag = find_option("detail",0,0)!=0;
   pickFlag = find_option("cherrypick",0,0)!=0;
+  backoutFlag = find_option("backout",0,0)!=0;
+  zBinGlob = find_option("binary",0,1);
   if( g.argc!=3 ){
     usage("VERSION");
   }
   db_must_be_within_tree();
+  if( zBinGlob==0 ) zBinGlob = db_get("binary-glob",0);
   vid = db_lget_int("checkout", 0);
   if( vid==0 ){
     fossil_fatal("nothing is checked out");
   }
   mid = name_to_rid(g.argv[2]);
@@ -67,14 +82,19 @@
     fossil_fatal("not a version: %s", g.argv[2]);
   }
   if( mid>1 && !db_exists("SELECT 1 FROM plink WHERE cid=%d", mid) ){
     fossil_fatal("not a version: %s", g.argv[2]);
   }
-  if( pickFlag ){
+  if( pickFlag || backoutFlag ){
     pid = db_int(0, "SELECT pid FROM plink WHERE cid=%d AND isprim", mid);
     if( pid<=0 ){
       fossil_fatal("cannot find an ancestor for %s", g.argv[2]);
+    }
+    if( backoutFlag ){
+      int t = pid;
+      pid = mid;
+      mid = t;
     }
   }else{
     pivot_set_primary(mid);
     pivot_set_secondary(vid);
     db_prepare(&q, "SELECT merge FROM vmerge WHERE id=0");
@@ -229,19 +249,21 @@
 
   /*
   ** Do a three-way merge on files that have changes pid->mid and pid->vid
   */
   db_prepare(&q,
-    "SELECT ridm, idv, ridp, ridv FROM fv"
+    "SELECT ridm, idv, ridp, ridv, %s FROM fv"
     " WHERE idp>0 AND idv>0 AND idm>0"
-    "   AND ridm!=ridp AND (ridv!=ridp OR chnged)"
+    "   AND ridm!=ridp AND (ridv!=ridp OR chnged)",
+    glob_expr("fv.fn", zBinGlob)
   );
   while( db_step(&q)==SQLITE_ROW ){
     int ridm = db_column_int(&q, 0);
     int idv = db_column_int(&q, 1);
     int ridp = db_column_int(&q, 2);
     int ridv = db_column_int(&q, 3);
+    int isBinary = db_column_int(&q, 4);
     int rc;
     char *zName = db_text(0, "SELECT pathname FROM vfile WHERE id=%d", idv);
     char *zFullPath;
     Blob m, p, v, r;
     /* Do a 3-way merge of idp->idm into idp->idv.  The results go into idv. */
@@ -254,11 +276,16 @@
     zFullPath = mprintf("%s/%s", g.zLocalRoot, zName);
     content_get(ridp, &p);
     content_get(ridm, &m);
     blob_zero(&v);
     blob_read_from_file(&v, zFullPath);
-    rc = blob_merge(&p, &m, &v, &r);
+    if( isBinary ){
+      rc = -1;
+      blob_zero(&r);
+    }else{
+      rc = blob_merge(&p, &m, &v, &r);
+    }
     if( rc>=0 ){
       blob_write_to_file(&r, zFullPath);
       if( rc>0 ){
         printf("***** %d merge conflicts in %s\n", rc, zName);
       }

Changes to src/name.c

@@ -113,13 +113,39 @@
   }
   return rc;
 }
 
 /*
+** Return TRUE if the string begins with an ISO8601 date: YYYY-MM-DD.
+*/
+static int is_date(const char *z){
+  if( !isdigit(z[0]) ) return 0;
+  if( !isdigit(z[1]) ) return 0;
+  if( !isdigit(z[2]) ) return 0;
+  if( !isdigit(z[3]) ) return 0;
+  if( z[4]!='-') return 0;
+  if( !isdigit(z[5]) ) return 0;
+  if( !isdigit(z[6]) ) return 0;
+  if( z[7]!='-') return 0;
+  if( !isdigit(z[8]) ) return 0;
+  if( !isdigit(z[9]) ) return 0;
+  return 1;
+}
+
+/*
 ** Convert a symbolic tag name into the UUID of a check-in that contains
 ** that tag.  If the tag appears on multiple check-ins, return the UUID
 ** of the most recent check-in with the tag.
+**
+** If the input string is of the form:
+**
+**      tag:date
+**
+** Then return the UUID of the oldest check-in with that tag that is
+** not older than 'date'.
+**
+** An input of "tip" returns the most recent check-in.
 **
 ** Memory to hold the returned string comes from malloc() and needs to
 ** be freed by the caller.
 */
 char *tag_to_uuid(const char *zTag){
@@ -132,10 +158,48 @@
        "   AND event.objid=tagxref.rid "
        "   AND blob.rid=event.objid "
        " ORDER BY event.mtime DESC ",
        zTag
     );
+  if( zUuid==0 ){
+    int nTag = strlen(zTag);
+    int i;
+    for(i=0; i<nTag-10; i++){
+      if( zTag[i]==':' && is_date(&zTag[i+1]) ){
+        char *zDate = mprintf("%s", &zTag[i+1]);
+        char *zTagBase = mprintf("%.*s", i, zTag);
+        int nDate = strlen(zDate);
+        int useUtc = 0;
+        if( sqlite3_strnicmp(&zDate[nDate-3],"utc",3)==0 ){
+          nDate -= 3;
+          zDate[nDate] = 0;
+          useUtc = 1;
+        }
+        zUuid = db_text(0,
+          "SELECT blob.uuid"
+          "  FROM tag, tagxref, event, blob"
+          " WHERE tag.tagname='sym-'||%Q "
+          "   AND tagxref.tagid=tag.tagid AND tagxref.tagtype>0 "
+          "   AND event.objid=tagxref.rid "
+          "   AND blob.rid=event.objid "
+          "   AND event.mtime<=julianday(%Q %s)"
+          " ORDER BY event.mtime DESC ",
+          zTagBase, zDate, (useUtc ? "" : ",'utc'")
+        );
+        break;
+      }
+    }
+    if( zUuid==0 && strcmp(zTag, "tip")==0 ){
+      zUuid = db_text(0,
+        "SELECT blob.uuid"
+        "  FROM event, blob"
+        " WHERE event.type='ci'"
+        "   AND blob.rid=event.objid"
+        " ORDER BY event.mtime DESC"
+      );
+    }
+  }
   return zUuid;
 }
 
 /*
 ** Convert a date/time string into a UUID.
@@ -162,11 +226,11 @@
   }else if( memcmp(zDate, "utc:", 4)==0 ){
     zDate += 4;
     useUtc = 1;
   }
   n = strlen(zDate);
-  if( n<10 || zDate[4]!='-' || zDate[7]!='-' ) return 0;
+  if( n<10 || !is_date(zDate) ) return 0;
   if( n>4 && sqlite3_strnicmp(&zDate[n-3], "utc", 3)==0 ){
     zCopy = mprintf("%s", zDate);
     zCopy[n-3] = 0;
     zDate = zCopy;
     n -= 3;

Changes to src/schema.c

@@ -309,10 +309,40 @@
 @   rid INTEGER REFERENCE blob,     -- Artifact tag is applied to
 @   UNIQUE(rid, tagid)
 @ );
 @ CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime);
 @
+@ -- When a hyperlink occurs from one artifact to another (for example
+@ -- when a check-in comment refers to a ticket) an entry is made in
+@ -- the following table for that hyperlink.  This table is used to
+@ -- facilitate the display of "back links".
+@ --
+@ CREATE TABLE backlink(
+@   target TEXT,           -- Where the hyperlink points to
+@   srctype INT,           -- 0: check-in  1: ticket  2: wiki
+@   srcid INT,             -- rid for checkin or wiki.  tkt_id for ticket.
+@   mtime TIMESTAMP,       -- time that the hyperlink was added
+@   UNIQUE(target, srctype, srcid)
+@ );
+@ CREATE INDEX backlink_src ON backlink(srcid, srctype);
+@
+@ -- Each attachment is an entry in the following table.  Only
+@ -- the most recent attachment (identified by the D card) is saved.
+@ --
+@ CREATE TABLE attachment(
+@   attachid INTEGER PRIMARY KEY,   -- Local id for this attachment
+@   isLatest BOOLEAN DEFAULT 0,     -- True if this is the one to use
+@   mtime TIMESTAMP,                -- Time when attachment last changed
+@   src TEXT,                       -- UUID of the attachment.  NULL to delete
+@   target TEXT,                    -- Object attached to. Wikiname or Tkt UUID
+@   filename TEXT,                  -- Filename for the attachment
+@   comment TEXT,                   -- Comment associated with this attachment
+@   user TEXT                       -- Name of user adding attachment
+@ );
+@ CREATE INDEX attachment_idx1 ON attachment(target, filename, mtime);
+@ CREATE INDEX attachment_idx2 ON attachment(src);
+@
 @ -- Template for the TICKET table
 @ --
 @ -- NB: when changing the schema of the TICKET table here, also make the
 @ -- same change in tktsetup.c.
 @ --
@@ -393,10 +423,11 @@
 @ CREATE TABLE vfile(
 @   id INTEGER PRIMARY KEY,           -- ID of the checked out file
 @   vid INTEGER REFERENCES blob,      -- The baseline this file is part of.
 @   chnged INT DEFAULT 0,             -- 0:unchnged 1:edited 2:m-chng 3:m-add
 @   deleted BOOLEAN DEFAULT 0,        -- True if deleted
+@   isexe BOOLEAN,                    -- True if file should be executable
 @   rid INTEGER,                      -- Originally from this repository record
 @   mrid INTEGER,                     -- Based on this record due to a merge
 @   mtime INTEGER,                    -- Modification time of file on disk
 @   pathname TEXT,                    -- Full pathname relative to root
 @   origname TEXT,                    -- Original pathname. NULL if unchanged

Changes to src/setup.c

@@ -145,10 +145,12 @@
   @ <ol>
   @ <li><p>The permission flags are as follows:</p>
   @ <table>
      @ <tr><td valign="top"><b>a</b></td>
      @   <td><i>Admin:</i> Create and delete users</td></tr>
+     @ <tr><td valign="top"><b>b</b></td>
+     @   <td><i>Attach:</i> Add attachments to wiki or tickets</td></tr>
      @ <tr><td valign="top"><b>c</b></td>
      @   <td><i>Append-Tkt:</i> Append to tickets</td></tr>
      @ <tr><td valign="top"><b>d</b></td>
      @   <td><i>Delete:</i> Delete wiki and tickets</td></tr>
      @ <tr><td valign="top"><b>e</b></td>
@@ -237,11 +239,11 @@
 */
 void user_edit(void){
   const char *zId, *zLogin, *zInfo, *zCap, *zPw;
   char *oaa, *oas, *oar, *oaw, *oan, *oai, *oaj, *oao, *oap;
   char *oak, *oad, *oac, *oaf, *oam, *oah, *oag, *oae;
-  char *oat, *oau, *oav, *oaz;
+  char *oat, *oau, *oav, *oab, *oaz;
   const char *inherit[128];
   int doWrite;
   int uid;
   int higherUser = 0;  /* True if user being edited is SETUP and the */
                        /* user doing the editing is ADMIN.  Disallow editing */
@@ -274,10 +276,11 @@
   doWrite = cgi_all("login","info","pw") && !higherUser;
   if( doWrite ){
     char zCap[50];
     int i = 0;
     int aa = P("aa")!=0;
+    int ab = P("ab")!=0;
     int ad = P("ad")!=0;
     int ae = P("ae")!=0;
     int ai = P("ai")!=0;
     int aj = P("aj")!=0;
     int ak = P("ak")!=0;
@@ -295,10 +298,11 @@
     int at = P("at")!=0;
     int au = P("au")!=0;
     int av = P("av")!=0;
     int az = P("az")!=0;
     if( aa ){ zCap[i++] = 'a'; }
+    if( ab ){ zCap[i++] = 'b'; }
     if( ac ){ zCap[i++] = 'c'; }
     if( ad ){ zCap[i++] = 'd'; }
     if( ae ){ zCap[i++] = 'e'; }
     if( af ){ zCap[i++] = 'f'; }
     if( ah ){ zCap[i++] = 'h'; }
@@ -351,18 +355,19 @@
   */
   zLogin = "";
   zInfo = "";
   zCap = "";
   zPw = "";
-  oaa = oac = oad = oae = oaf = oag = oah = oai = oaj = oak = oam =
+  oaa = oab = oac = oad = oae = oaf = oag = oah = oai = oaj = oak = oam =
         oan = oao = oap = oar = oas = oat = oau = oav = oaw = oaz = "";
   if( uid ){
     zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid);
     zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid);
     zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", uid);
     zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid);
     if( strchr(zCap, 'a') ) oaa = " checked";
+    if( strchr(zCap, 'b') ) oab = " checked";
     if( strchr(zCap, 'c') ) oac = " checked";
     if( strchr(zCap, 'd') ) oad = " checked";
     if( strchr(zCap, 'e') ) oae = " checked";
     if( strchr(zCap, 'f') ) oaf = " checked";
     if( strchr(zCap, 'g') ) oag = " checked";
@@ -465,15 +470,16 @@
   @    <input type="checkbox" name="ag"%s(oag)/>%s(B('g'))Clone<br>
   @    <input type="checkbox" name="aj"%s(oaj)/>%s(B('j'))Read Wiki<br>
   @    <input type="checkbox" name="af"%s(oaf)/>%s(B('f'))New Wiki<br>
   @    <input type="checkbox" name="am"%s(oam)/>%s(B('m'))Append Wiki<br>
   @    <input type="checkbox" name="ak"%s(oak)/>%s(B('k'))Write Wiki<br>
-  @    <input type="checkbox" name="ar"%s(oar)/>%s(B('r'))Read Tkt<br>
-  @    <input type="checkbox" name="an"%s(oan)/>%s(B('n'))New Tkt<br>
-  @    <input type="checkbox" name="ac"%s(oac)/>%s(B('c'))Append Tkt<br>
-  @    <input type="checkbox" name="aw"%s(oaw)/>%s(B('w'))Write Tkt<br>
-  @    <input type="checkbox" name="at"%s(oat)/>%s(B('t'))Tkt Report<br>
+  @    <input type="checkbox" name="ab"%s(oab)/>%s(B('b'))Attachments<br>
+  @    <input type="checkbox" name="ar"%s(oar)/>%s(B('r'))Read Ticket<br>
+  @    <input type="checkbox" name="an"%s(oan)/>%s(B('n'))New Ticket<br>
+  @    <input type="checkbox" name="ac"%s(oac)/>%s(B('c'))Append Ticket<br>
+  @    <input type="checkbox" name="aw"%s(oaw)/>%s(B('w'))Write Ticket<br>
+  @    <input type="checkbox" name="at"%s(oat)/>%s(B('t'))Ticket Report<br>
   @    <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip
   @   </td>
   @ </tr>
   @ <tr>
   @   <td align="right">Password:</td>
@@ -562,13 +568,13 @@
   @ </li><p>
   @
   @ <li><p>
   @ The <b>Read Wiki</b>, <b>New Wiki</b>, <b>Append Wiki</b>, and
   @ <b>Write Wiki</b> privileges control access to wiki pages.  The
-  @ <b>Read Tkt</b>, <b>New Tkt</b>, <b>Append Tkt</b>, and
-  @ <b>Write Tkt</b> privileges control access to trouble tickets.
-  @ The <b>Tkt Report</b> privilege allows the user to create or edit
+  @ <b>Read Ticket</b>, <b>New Ticket</b>, <b>Append Ticket</b>, and
+  @ <b>Write Ticket</b> privileges control access to trouble tickets.
+  @ The <b>Ticket Report</b> privilege allows the user to create or edit
   @ ticket report formats.
   @ </p></li>
   @
   @ <li><p>
   @ Users with the <b>Password</b> privilege are allowed to change their
@@ -580,10 +586,15 @@
   @ The <b>EMail</b> privilege allows the display of sensitive information
   @ such as the email address of users and contact information on tickets.
   @ Recommended OFF for "anonymous" and for "nobody" but ON for
   @ "developer".
   @ </p></li>
+  @
+  @ <li><p>
+  @ The <b>Attachment</b> privilege is needed in order to add attachments
+  @ to tickets or wiki.  Write privilege on the ticket or wiki is also
+  @ required.</p></li>
   @
   @ <li><p>
   @ Login is prohibited if the password is an empty string.
   @ </p></li>
   @ </ul>

Changes to src/skins.c

@@ -162,11 +162,11 @@
 @ REPLACE INTO config VALUES('header','<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$baseurl/timeline.rss">
-@ <link rel="stylesheet" href="$baseurl/style.css" type="text/css"
+@ <link rel="stylesheet" href="$baseurl/style.css?blackwhite" type="text/css"
 @       media="screen">
 @ </head>
 @ <body>
 @ <div class="header">
 @   <div class="logo">
@@ -182,37 +182,37 @@
 @        puts "Not logged in"
 @      }
 @   </th1></nobr></div>
 @ </div>
 @ <div class="mainmenu"><th1>
-@ html "<a href="$baseurl$index_page">Home</a> "
+@ html "<a href=''$baseurl$index_page''>Home</a> "
 @ if {[anycap jor]} {
-@   html "<a href="$baseurl/timeline">Timeline</a> "
+@   html "<a href=''$baseurl/timeline''>Timeline</a> "
 @ }
 @ if {[hascap oh]} {
-@   html "<a href="$baseurl/dir">Files</a> "
+@   html "<a href=''$baseurl/dir?ci=tip''>Files</a> "
 @ }
 @ if {[hascap o]} {
-@   html "<a href="$baseurl/leaves">Leaves</a> "
-@   html "<a href="$baseurl/brlist">Branches</a> "
-@   html "<a href="$baseurl/taglist">Tags</a> "
+@   html "<a href=''$baseurl/leaves''>Leaves</a> "
+@   html "<a href=''$baseurl/brlist''>Branches</a> "
+@   html "<a href=''$baseurl/taglist''>Tags</a> "
 @ }
 @ if {[hascap r]} {
-@   html "<a href="$baseurl/reportlist">Tickets</a> "
+@   html "<a href=''$baseurl/reportlist''>Tickets</a> "
 @ }
 @ if {[hascap j]} {
-@   html "<a href="$baseurl/wiki">Wiki</a> "
+@   html "<a href=''$baseurl/wiki''>Wiki</a> "
 @ }
 @ if {[hascap s]} {
-@   html "<a href="$baseurl/setup">Admin</a> "
+@   html "<a href=''$baseurl/setup''>Admin</a> "
 @ } elseif {[hascap a]} {
-@   html "<a href="$baseurl/setup_ulist">Users</a> "
+@   html "<a href=''$baseurl/setup_ulist''>Users</a> "
 @ }
 @ if {[info exists login]} {
-@   html "<a href="$baseurl/login">Logout</a> "
+@   html "<a href=''$baseurl/login''>Logout</a> "
 @ } else {
-@   html "<a href="$baseurl/login">Login</a> "
+@   html "<a href=''$baseurl/login''>Login</a> "
 @ }
 @ </th1></div>
 @ ');
 @ REPLACE INTO config VALUES('footer','<div class="footer">
 @ Fossil version $manifest_version $manifest_date
@@ -367,11 +367,11 @@
 @ REPLACE INTO config VALUES('header','<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$baseurl/timeline.rss">
-@ <link rel="stylesheet" href="$baseurl/style.css" type="text/css"
+@ <link rel="stylesheet" href="$baseurl/style.css?tan" type="text/css"
 @       media="screen">
 @ </head>
 @ <body>
 @ <div class="header">
 @   <div class="title">$<title></div>
@@ -384,37 +384,37 @@
 @        puts "Not logged in"
 @      }
 @   </th1></nobr></div>
 @ </div>
 @ <div class="mainmenu"><th1>
-@ html "<a href="$baseurl$index_page">Home</a> "
+@ html "<a href=''$baseurl$index_page''>Home</a> "
 @ if {[anycap jor]} {
-@   html "<a href="$baseurl/timeline">Timeline</a> "
+@   html "<a href=''$baseurl/timeline''>Timeline</a> "
 @ }
 @ if {[hascap oh]} {
-@   html "<a href="$baseurl/dir">Files</a> "
+@   html "<a href=''$baseurl/dir?ci=tip''>Files</a> "
 @ }
 @ if {[hascap o]} {
-@   html "<a href="$baseurl/leaves">Leaves</a> "
-@   html "<a href="$baseurl/brlist">Branches</a> "
-@   html "<a href="$baseurl/taglist">Tags</a> "
+@   html "<a href=''$baseurl/leaves''>Leaves</a> "
+@   html "<a href=''$baseurl/brlist''>Branches</a> "
+@   html "<a href=''$baseurl/taglist''>Tags</a> "
 @ }
 @ if {[hascap r]} {
-@   html "<a href="$baseurl/reportlist">Tickets</a> "
+@   html "<a href=''$baseurl/reportlist''>Tickets</a> "
 @ }
 @ if {[hascap j]} {
-@   html "<a href="$baseurl/wiki">Wiki</a> "
+@   html "<a href=''$baseurl/wiki''>Wiki</a> "
 @ }
 @ if {[hascap s]} {
-@   html "<a href="$baseurl/setup">Admin</a> "
+@   html "<a href=''$baseurl/setup''>Admin</a> "
 @ } elseif {[hascap a]} {
-@   html "<a href="$baseurl/setup_ulist">Users</a> "
+@   html "<a href=''$baseurl/setup_ulist''>Users</a> "
 @ }
 @ if {[info exists login]} {
-@   html "<a href="$baseurl/login">Logout</a> "
+@   html "<a href=''$baseurl/login''>Logout</a> "
 @ } else {
-@   html "<a href="$baseurl/login">Login</a> "
+@   html "<a href=''$baseurl/login''>Login</a> "
 @ }
 @ </th1></div>
 @ ');
 @ REPLACE INTO config VALUES('footer','<div class="footer">
 @ Fossil version $manifest_version $manifest_date
@@ -514,11 +514,11 @@
 @ div.mainmenu a:hover {
 @   color: #eee;
 @   background-color: #333;
 @ }
 @
-@ /* Container for the sub-menu and content so they don"t spread
+@ /* Container for the sub-menu and content so they don''t spread
 @ ** out underneath the main menu */
 @ #container {
 @   padding-left: 9em;
 @ }
 @
@@ -600,11 +600,11 @@
 @ REPLACE INTO config VALUES('header','<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$baseurl/timeline.rss">
-@ <link rel="stylesheet" href="$baseurl/style.css" type="text/css"
+@ <link rel="stylesheet" href="$baseurl/style.css?black2" type="text/css"
 @       media="screen">
 @ </head>
 @ <body>
 @ <div class="header">
 @   <div class="logo">
@@ -619,37 +619,37 @@
 @        puts "Not logged in"
 @      }
 @   </th1></nobr></div>
 @ </div>
 @ <div class="mainmenu"><ul><th1>
-@ html "<li><a href="$baseurl$index_page">Home</a></li>"
+@ html "<li><a href=''$baseurl$index_page''>Home</a></li>"
 @ if {[anycap jor]} {
-@   html "<li><a href="$baseurl/timeline">Timeline</a></li>"
+@   html "<li><a href=''$baseurl/timeline''>Timeline</a></li>"
 @ }
 @ if {[hascap oh]} {
-@   html "<li><a href="$baseurl/dir">Files</a></li>"
+@   html "<li><a href=''$baseurl/dir?ci=tip''>Files</a></li>"
 @ }
 @ if {[hascap o]} {
-@   html "<li><a href="$baseurl/leaves">Leaves</a></li>"
-@   html "<li><a href="$baseurl/brlist">Branches</a></li>"
-@   html "<li><a href="$baseurl/taglist">Tags</a></li>"
+@   html "<li><a href=''$baseurl/leaves''>Leaves</a></li>"
+@   html "<li><a href=''$baseurl/brlist''>Branches</a></li>"
+@   html "<li><a href=''$baseurl/taglist''>Tags</a></li>"
 @ }
 @ if {[hascap r]} {
-@   html "<li><a href="$baseurl/reportlist">Tickets</a></li>"
+@   html "<li><a href=''$baseurl/reportlist''>Tickets</a></li>"
 @ }
 @ if {[hascap j]} {
-@   html "<li><a href="$baseurl/wiki">Wiki</a></li>"
+@   html "<li><a href=''$baseurl/wiki''>Wiki</a></li>"
 @ }
 @ if {[hascap s]} {
-@   html "<li><a href="$baseurl/setup">Admin</a></li>"
+@   html "<li><a href=''$baseurl/setup''>Admin</a></li>"
 @ } elseif {[hascap a]} {
-@   html "<li><a href="$baseurl/setup_ulist">Users</a></li>"
+@   html "<li><a href=''$baseurl/setup_ulist''>Users</a></li>"
 @ }
 @ if {[info exists login]} {
-@   html "<li><a href="$baseurl/login">Logout</a></li>"
+@   html "<li><a href=''$baseurl/login''>Logout</a></li>"
 @ } else {
-@   html "<li><a href="$baseurl/login">Login</a></li>"
+@   html "<li><a href=''$baseurl/login''>Login</a></li>"
 @ }
 @ </th1></ul></div>
 @ <div id="container">
 @ ');
 @ REPLACE INTO config VALUES('footer','</div>

Changes to src/style.c

@@ -188,11 +188,11 @@
 @ <html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$baseurl/timeline.rss">
-@ <link rel="stylesheet" href="$baseurl/style.css" type="text/css"
+@ <link rel="stylesheet" href="$baseurl/style.css?default" type="text/css"
 @       media="screen">
 @ </head>
 @ <body>
 @ <div class="header">
 @   <div class="logo">
@@ -212,11 +212,11 @@
 @ html "<a href='$baseurl$index_page'>Home</a> "
 @ if {[anycap jor]} {
 @   html "<a href='$baseurl/timeline'>Timeline</a> "
 @ }
 @ if {[hascap oh]} {
-@   html "<a href='$baseurl/dir'>Files</a> "
+@   html "<a href='$baseurl/dir?ci=tip'>Files</a> "
 @ }
 @ if {[hascap o]} {
 @   html "<a href='$baseurl/leaves'>Leaves</a> "
 @   html "<a href='$baseurl/brlist'>Branches</a> "
 @   html "<a href='$baseurl/taglist'>Tags</a> "

Changes to src/tag.c

@@ -200,10 +200,15 @@
       break;
     }
   }
   if( zCol ){
     db_multi_exec("UPDATE event SET %s=%Q WHERE objid=%d", zCol, zValue, rid);
+    if( tagid==TAG_COMMENT ){
+      char *zCopy = mprintf("%s", zValue);
+      wiki_extract_links(zCopy, rid, 0, mtime, 1, WIKI_INLINE);
+      free(zCopy);
+    }
   }
   if( tagid==TAG_DATE ){
     db_multi_exec("UPDATE event SET mtime=julianday(%Q) WHERE objid=%d",
                   zValue, rid);
   }
@@ -563,11 +568,11 @@
   if( !g.okRead ){ login_needed(); return; }
 
   style_header("Tagged Check-ins");
   style_submenu_element("List", "List", "taglist");
   login_anonymous_available();
-  @ <h2>Check-ins with non-propagating tags:</t2>
+  @ <h2>Check-ins with non-propagating tags:</h2>
   db_prepare(&q,
     "%s 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-*'))"

Changes to src/timeline.c

@@ -200,14 +200,10 @@
   if( tmFlags & TIMELINE_GRAPH ){
     pGraph = graph_init();
     @ <div id="canvas" style="position:relative;width:1px;height:1px;"></div>
   }
 
-  db_multi_exec(
-     "CREATE TEMP TABLE IF NOT EXISTS seen(rid INTEGER PRIMARY KEY);"
-     "DELETE FROM seen;"
-  );
   @ <table cellspacing=0 border=0 cellpadding=0>
   blob_zero(&comment);
   while( db_step(pQuery)==SQLITE_ROW ){
     int rid = db_column_int(pQuery, 0);
     const char *zUuid = db_column_text(pQuery, 1);
@@ -241,11 +237,10 @@
     }
     if( strcmp(zType,"div")==0 ){
       @ <tr><td colspan=3><hr></td></tr>
       continue;
     }
-    db_multi_exec("INSERT OR IGNORE INTO seen VALUES(%d)", rid);
     if( memcmp(zDate, zPrevDate, 10) ){
       sprintf(zPrevDate, "%.10s", zDate);
       @ <tr><td>
       @   <div class="divider"><nobr>%s(zPrevDate)</nobr></div>
       @ </td></tr>
@@ -253,11 +248,39 @@
     memcpy(zTime, &zDate[11], 5);
     zTime[5] = 0;
     @ <tr>
     @ <td valign="top" align="right">%s(zTime)</td>
     @ <td width="20" align="left" valign="top">
-    @ <div id="m%d(rid)"></div>
+    if( pGraph && zType[0]=='c' ){
+      int nParent = 0;
+      int aParent[32];
+      const char *zBr;
+      int gidx;
+      static Stmt qparent;
+      static Stmt qbranch;
+      db_static_prepare(&qparent,
+        "SELECT pid FROM plink WHERE cid=:rid ORDER BY isprim DESC"
+      );
+      db_static_prepare(&qbranch,
+        "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
+        TAG_BRANCH
+      );
+      db_bind_int(&qparent, ":rid", rid);
+      while( db_step(&qparent)==SQLITE_ROW && nParent<32 ){
+        aParent[nParent++] = db_column_int(&qparent, 0);
+      }
+      db_reset(&qparent);
+      db_bind_int(&qbranch, ":rid", rid);
+      if( db_step(&qbranch)==SQLITE_ROW ){
+        zBr = db_column_text(&qbranch, 0);
+      }else{
+        zBr = "trunk";
+      }
+      gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr);
+      db_reset(&qbranch);
+      @ <div id="m%d(gidx)"></div>
+    }
     if( zBgClr && zBgClr[0] ){
       @ <td valign="top" align="left" bgcolor="%h(zBgClr)">
     }else{
       @ <td valign="top" align="left">
     }
@@ -289,37 +312,10 @@
       if( nTag>0 ){
         int i;
         for(i=0; i<nTag; i++){
           @ <b>%s(azTag[i])%s(i==nTag-1?"":",")</b>
         }
-      }
-      if( pGraph ){
-        int nParent = 0;
-        int aParent[32];
-        const char *zBr;
-        static Stmt qparent;
-        static Stmt qbranch;
-        db_static_prepare(&qparent,
-          "SELECT pid FROM plink WHERE cid=:rid ORDER BY isprim DESC"
-        );
-        db_static_prepare(&qbranch,
-          "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
-          TAG_BRANCH
-        );
-        db_bind_int(&qparent, ":rid", rid);
-        while( db_step(&qparent)==SQLITE_ROW && nParent<32 ){
-          aParent[nParent++] = db_column_int(&qparent, 0);
-        }
-        db_reset(&qparent);
-        db_bind_int(&qbranch, ":rid", rid);
-        if( db_step(&qbranch)==SQLITE_ROW ){
-          zBr = db_column_text(&qbranch, 0);
-        }else{
-          zBr = "trunk";
-        }
-        graph_add_row(pGraph, rid, isLeaf, nParent, aParent, zBr);
-        db_reset(&qbranch);
       }
     }else if( (tmFlags & TIMELINE_ARTID)!=0 ){
       hyperlink_to_uuid(zUuid);
     }
     db_column_blob(pQuery, commentColumn, &comment);
@@ -358,19 +354,27 @@
     }else{
       @ <tr><td><td><div style="width:%d(pGraph->mxRail*20+30)px;"></div>
     }
   }
   @ </table>
+  timeline_output_graph_javascript(pGraph);
+}
+
+/*
+** Generate all of the necessary javascript to generate a timeline
+** graph.
+*/
+void timeline_output_graph_javascript(GraphContext *pGraph){
   if( pGraph && pGraph->nErr==0 ){
     GraphRow *pRow;
     int i;
     char cSep;
     @ <script type="text/JavaScript">
     cgi_printf("var rowinfo = [\n");
     for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
       cgi_printf("{id:\"m%d\",r:%d,d:%d,mo:%d,mu:%d,u:%d,au:",
-        pRow->rid,
+        pRow->idx,
         pRow->iRail,
         pRow->bDescender,
         pRow->mergeOut,
         pRow->mergeUpto,
         pRow->aiRaiser[pRow->iRail]
@@ -698,11 +702,11 @@
   if( zType[0]=='a' ){
     tmFlags = TIMELINE_BRIEF | TIMELINE_GRAPH;
   }else{
     tmFlags = TIMELINE_GRAPH;
   }
-  if( P("ng")!=0 ){
+  if( P("ng")!=0 || zSearch!=0 ){
     tmFlags &= ~TIMELINE_GRAPH;
   }
 
   style_header("Timeline");
   login_anonymous_available();
@@ -890,10 +894,13 @@
       blob_appendf(&desc, " occurring on or after %h.<br>", zAfter);
     }else if( zBefore ){
       blob_appendf(&desc, " occurring on or before %h.<br>", zBefore);
     }else if( zCirca ){
       blob_appendf(&desc, " occurring around %h.<br>", zCirca);
+    }
+    if( zSearch ){
+      blob_appendf(&desc, " matching \"%h\"", zSearch);
     }
     if( g.okHistory ){
       if( zAfter || n==nEntry ){
         zDate = db_text(0, "SELECT min(timestamp) FROM timeline");
         timeline_submenu(&url, "Older", "b", zDate, "a");

Changes to src/tkt.c

@@ -166,42 +166,20 @@
     Th_Store(z, P(z));
   }
 }
 
 /*
-** Rebuild all tickets named in the _pending_ticket table.
-**
-** This routine is called just prior to commit after new
-** out-of-sequence ticket changes have been added.
-*/
-static int ticket_rebuild_at_commit(void){
-  Stmt q;
-  db_multi_exec(
-    "DELETE FROM ticket WHERE tkt_uuid IN _pending_ticket"
-  );
-  db_prepare(&q, "SELECT uuid FROM _pending_ticket");
-  while( db_step(&q)==SQLITE_ROW ){
-    const char *zUuid = db_column_text(&q, 0);
-    ticket_rebuild_entry(zUuid);
-  }
-  db_multi_exec(
-    "DELETE FROM _pending_ticket"
-  );
-  return 0;
-}
-
-/*
 ** Update an entry of the TICKET table according to the information
 ** in the control file given in p.  Attempt to create the appropriate
 ** TICKET table entry if createFlag is true.  If createFlag is false,
 ** that means we already know the entry exists and so we can save the
 ** work of trying to create it.
 **
 ** Return TRUE if a new TICKET entry was created and FALSE if an
 ** existing entry was revised.
 */
-int ticket_insert(const Manifest *p, int createFlag, int checkTime){
+int ticket_insert(const Manifest *p, int createFlag, int rid){
   Blob sql;
   Stmt q;
   int i;
   const char *zSep;
   int rc = 0;
@@ -224,27 +202,20 @@
                    zName, zName, p->aField[i].zValue);
     }else{
       if( fieldId(zName)<0 ) continue;
       blob_appendf(&sql,", %s=%Q", zName, p->aField[i].zValue);
     }
+    if( rid>0 ){
+      wiki_extract_links(p->aField[i].zValue, rid, 1, p->rDate, i==0, 0);
+    }
   }
   blob_appendf(&sql, " WHERE tkt_uuid='%s' AND tkt_mtime<:mtime",
                      p->zTicketUuid);
   db_prepare(&q, "%s", blob_str(&sql));
   db_bind_double(&q, ":mtime", p->rDate);
   db_step(&q);
   db_finalize(&q);
-  if( checkTime && db_changes()==0 ){
-    static int isInit = 0;
-    if( !isInit ){
-      db_multi_exec("CREATE TEMP TABLE _pending_ticket(uuid TEXT UNIQUE)");
-      db_commit_hook(ticket_rebuild_at_commit, 1);
-      isInit = 1;
-    }
-    db_multi_exec("INSERT OR IGNORE INTO _pending_ticket "
-                  "VALUES(%Q)", p->zTicketUuid);
-  }
   blob_reset(&sql);
   return rc;
 }
 
 /*
@@ -264,11 +235,11 @@
   db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid);
   while( db_step(&q)==SQLITE_ROW ){
     int rid = db_column_int(&q, 0);
     content_get(rid, &content);
     manifest_parse(&manifest, &content);
-    ticket_insert(&manifest, createFlag, 0);
+    ticket_insert(&manifest, createFlag, rid);
     manifest_ticket_event(rid, &manifest, createFlag, tagid);
     manifest_clear(&manifest);
     createFlag = 0;
   }
   db_finalize(&q);
@@ -325,35 +296,79 @@
 **
 ** View a ticket.
 */
 void tktview_page(void){
   const char *zScript;
+  char *zFullName;
+  const char *zUuid = PD("name","");
+
   login_check_credentials();
   if( !g.okRdTkt ){ login_needed(); return; }
   if( g.okWrTkt || g.okApndTkt ){
     style_submenu_element("Edit", "Edit The Ticket", "%s/tktedit?name=%T",
         g.zTop, PD("name",""));
   }
   if( g.okHistory ){
-    const char *zUuid = PD("name","");
     style_submenu_element("History", "History Of This Ticket",
         "%s/tkthistory/%T", g.zTop, zUuid);
     style_submenu_element("Timeline", "Timeline Of This Ticket",
         "%s/tkttimeline/%T", g.zTop, zUuid);
+    style_submenu_element("Check-ins", "Check-ins Of This Ticket",
+        "%s/tkttimeline/%T?y=ci", g.zTop, zUuid);
   }
   if( g.okNewTkt ){
     style_submenu_element("New Ticket", "Create a new ticket",
         "%s/tktnew", g.zTop);
+  }
+  if( g.okApndTkt && g.okAttach ){
+    style_submenu_element("Attach", "Add An Attachment",
+        "%s/attachadd?tkt=%T&from=%s/tktview%%3fname=%t",
+        g.zTop, zUuid, g.zTop, zUuid);
   }
   style_header("View Ticket");
   if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br />\n", -1);
   ticket_init();
   initializeVariablesFromDb();
   zScript = ticket_viewpage_code();
   if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW_SCRIPT<br />\n", -1);
   Th_Render(zScript);
   if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1);
+
+  zFullName = db_text(0,
+       "SELECT tkt_uuid FROM ticket"
+       " WHERE tkt_uuid GLOB '%q*'", zUuid);
+  if( zFullName ){
+    int cnt = 0;
+    Stmt q;
+    db_prepare(&q,
+       "SELECT datetime(mtime,'localtime'), filename, user"
+       "  FROM attachment"
+       " WHERE isLatest AND src!='' AND target=%Q"
+       " ORDER BY mtime DESC",
+       zFullName);
+    while( db_step(&q)==SQLITE_ROW ){
+      const char *zDate = db_column_text(&q, 0);
+      const char *zFile = db_column_text(&q, 1);
+      const char *zUser = db_column_text(&q, 2);
+      if( cnt==0 ){
+        @ <hr><h2>Attachments:</h2>
+        @ <ul>
+      }
+      cnt++;
+      @ <li><a href="%s(g.zTop)/attachview?tkt=%s(zFullName)&file=%t(zFile)">
+      @ %h(zFile)</a> add by %h(zUser) on
+      hyperlink_to_date(zDate, ".");
+      if( g.okWrTkt && g.okAttach ){
+        @ [<a href="%s(g.zTop)/attachdelete?tkt=%s(zFullName)&file=%t(zFile)&from=%s(g.zTop)/tktview%%3fname=%s(zFullName)">delete</a>]
+      }
+    }
+    if( cnt ){
+      @ </ul>
+    }
+    db_finalize(&q);
+  }
+
   style_footer();
 }
 
 /*
 ** TH command:   append_field FIELD STRING
@@ -610,42 +625,78 @@
   return 0;
 }
 
 /*
 ** WEBPAGE: tkttimeline
-** URL: /tkttimeline?name=TICKETUUID
+** URL: /tkttimeline?name=TICKETUUID&y=TYPE
 **
 ** Show the change history for a single ticket in timeline format.
 */
 void tkttimeline_page(void){
   Stmt q;
   char *zTitle;
   char *zSQL;
   const char *zUuid;
+  char *zFullUuid;
   int tagid;
+  char zGlobPattern[50];
+  const char *zType;
 
   login_check_credentials();
   if( !g.okHistory || !g.okRdTkt ){ login_needed(); return; }
   zUuid = PD("name","");
+  zType = PD("y","a");
+  if( zType[0]!='c' ){
+    style_submenu_element("Check-ins", "Check-ins",
+       "%s/tkttimeline?name=%T&y=ci", g.zTop, zUuid);
+  }else{
+    style_submenu_element("Timeline", "Timeline",
+       "%s/tkttimeline?name=%T", g.zTop, zUuid);
+  }
   style_submenu_element("History", "History",
     "%s/tkthistory/%s", g.zTop, zUuid);
   style_submenu_element("Status", "Status",
     "%s/info/%s", g.zTop, zUuid);
-  zTitle = mprintf("Timeline Of Ticket %h", zUuid);
+  if( zType[0]=='c' ){
+    zTitle = mprintf("Check-Ins Associated With Ticket %h", zUuid);
+  }else{
+    zTitle = mprintf("Timeline Of Ticket %h", zUuid);
+  }
   style_header(zTitle);
   free(zTitle);
 
+  sqlite3_snprintf(6, zGlobPattern, "%s", zUuid);
+  canonical16(zGlobPattern, strlen(zGlobPattern));
   tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid);
   if( tagid==0 ){
     @ No such ticket: %h(zUuid)
     style_footer();
     return;
   }
-  zSQL = mprintf("%s AND event.objid IN "
-                 "  (SELECT rid FROM tagxref WHERE tagid=%d) "
-                 "ORDER BY mtime DESC",
-                 timeline_query_for_www(), tagid);
+  zFullUuid = db_text(0, "SELECT substr(tagname, 5) FROM tag WHERE tagid=%d",
+                         tagid);
+  if( zType[0]=='c' ){
+    zSQL = mprintf(
+         "%s AND event.objid IN "
+         "   (SELECT srcid FROM backlink WHERE target GLOB '%.4s*' "
+                                         "AND '%s' GLOB (target||'*')) "
+         "ORDER BY mtime DESC",
+         timeline_query_for_www(), zFullUuid, zFullUuid
+    );
+  }else{
+    zSQL = mprintf(
+         "%s AND event.objid IN "
+         "  (SELECT rid FROM tagxref WHERE tagid=%d"
+         "   UNION SELECT srcid FROM backlink"
+                  " WHERE target GLOB '%.4s*'"
+                  "   AND '%s' GLOB (target||'*')"
+         "   UNION SELECT attachid FROM attachment"
+                  " WHERE target=%Q) "
+         "ORDER BY mtime DESC",
+         timeline_query_for_www(), tagid, zFullUuid, zFullUuid, zFullUuid
+    );
+  }
   db_prepare(&q, zSQL);
   free(zSQL);
   www_print_timeline(&q, TIMELINE_ARTID, 0);
   db_finalize(&q);
   style_footer();
@@ -667,10 +718,12 @@
   if( !g.okHistory || !g.okRdTkt ){ login_needed(); return; }
   zUuid = PD("name","");
   zTitle = mprintf("History Of Ticket %h", zUuid);
   style_submenu_element("Status", "Status",
     "%s/info/%s", g.zTop, zUuid);
+  style_submenu_element("Check-ins", "Check-ins",
+    "%s/tkttimeline?name=%s?y=ci", g.zTop, zUuid);
   style_submenu_element("Timeline", "Timeline",
     "%s/tkttimeline?name=%s", g.zTop, zUuid);
   style_header(zTitle);
   free(zTitle);
 
@@ -679,37 +732,60 @@
     @ No such ticket: %h(zUuid)
     style_footer();
     return;
   }
   db_prepare(&q,
-    "SELECT objid, uuid FROM event, blob"
+    "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"
-    " ORDER BY mtime DESC",
-    tagid
+    " 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 ){
     Blob content;
     Manifest m;
-    int rid = db_column_int(&q, 0);
-    const char *zChngUuid = db_column_text(&q, 1);
-    content_get(rid, &content);
-    if( manifest_parse(&m, &content) && m.type==CFTYPE_TICKET ){
-      char *zDate = db_text(0, "SELECT datetime(%.12f)", m.rDate);
-      char zUuid[12];
-      memcpy(zUuid, zChngUuid, 10);
-      zUuid[10] = 0;
-      @
-      @ Ticket change
-      @ [<a href="%s(g.zTop)/artifact/%T(zChngUuid)">%s(zUuid)</a>]</a>
+    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 ){
+        @
+        @ <p>Delete attachment "%h(zFile)"
+      }else{
+        @
+        @ <p>Add attachment "%h(zFile)"
+      }
+      @ [<a href="%s(g.zTop)/artifact/%T(zChngUuid)">%s(zShort)</a>]
       @ (rid %d(rid)) by
-      hyperlink_to_user(m.zUser,zDate," on");
-      hyperlink_to_date(zDate, ":");
-      free(zDate);
-      ticket_output_change_artifact(&m);
+      hyperlink_to_user(zUser,zDate," on");
+      hyperlink_to_date(zDate, ".</p>");
+    }else{
+      content_get(rid, &content);
+      if( manifest_parse(&m, &content) && m.type==CFTYPE_TICKET ){
+        @
+        @ <p>Ticket change
+        @ [<a href="%s(g.zTop)/artifact/%T(zChngUuid)">%s(zShort)</a>]
+        @ (rid %d(rid)) by
+        hyperlink_to_user(m.zUser,zDate," on");
+        hyperlink_to_date(zDate, ":");
+        ticket_output_change_artifact(&m);
+        @ </p>
+      }
+      manifest_clear(&m);
     }
-    manifest_clear(&m);
   }
   db_finalize(&q);
   style_footer();
 }
 

Changes to src/wiki.c

@@ -127,10 +127,12 @@
   int isSandbox;
   Blob wiki;
   Manifest m;
   const char *zPageName;
   char *zBody = mprintf("%s","<i>Empty Page</i>");
+  Stmt q;
+  int cnt = 0;
 
   login_check_credentials();
   if( !g.okRdWiki ){ login_needed(); return; }
   zPageName = P("name");
   if( zPageName==0 ){
@@ -152,11 +154,12 @@
       @ <li>  Create a <a href="%s(g.zBaseURL)/wikinew">new wiki page</a>.</li>
     }
     @ <li> <a href="%s(g.zBaseURL)/wcontent">List of All Wiki Pages</a>
     @      available on this server.</li>
 	@ <li> <form method="GET" action="%s(g.zBaseURL)/wfind">
-	@     Search wiki titles: <input type="text" name="title"/> &nbsp; <input type="submit" />
+	@     Search wiki titles: <input type="text" name="title"/>
+        @  &nbsp; <input type="submit" />
 	@ </li>
     @ </ul>
     style_footer();
     return;
   }
@@ -176,19 +179,25 @@
     blob_zero(&m.content);
     if( rid ){
       Blob content;
       content_get(rid, &content);
       manifest_parse(&m, &content);
-      if( m.type==CFTYPE_WIKI ){
-        zBody = m.zWiki;
+      if( m.type==CFTYPE_WIKI && m.zWiki ){
+        while( isspace(m.zWiki[0]) ) m.zWiki++;
+        if( m.zWiki[0] ) zBody = m.zWiki;
       }
     }
   }
   if( !g.isHome ){
     if( (rid && g.okWrWiki) || (!rid && g.okNewWiki) ){
       style_submenu_element("Edit", "Edit Wiki Page", "%s/wikiedit?name=%T",
            g.zTop, zPageName);
+    }
+    if( rid && g.okApndWiki && g.okAttach ){
+      style_submenu_element("Attach", "Add An Attachment",
+           "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
+           g.zTop, zPageName, g.zTop, zPageName);
     }
     if( rid && g.okApndWiki ){
       style_submenu_element("Append", "Add A Comment", "%s/wikiappend?name=%T",
            g.zTop, zPageName);
     }
@@ -199,10 +208,42 @@
   }
   style_header(zPageName);
   blob_init(&wiki, zBody, -1);
   wiki_convert(&wiki, 0, 0);
   blob_reset(&wiki);
+
+  db_prepare(&q,
+     "SELECT datetime(mtime,'localtime'), filename, user"
+     "  FROM attachment"
+     " WHERE isLatest AND src!='' AND target=%Q"
+     " ORDER BY mtime DESC",
+     zPageName);
+  while( db_step(&q)==SQLITE_ROW ){
+    const char *zDate = db_column_text(&q, 0);
+    const char *zFile = db_column_text(&q, 1);
+    const char *zUser = db_column_text(&q, 2);
+    if( cnt==0 ){
+      @ <hr><h2>Attachments:</h2>
+      @ <ul>
+    }
+    cnt++;
+    if( g.okHistory ){
+      @ <li><a href="%s(g.zTop)/attachview?page=%s(zPageName)&file=%t(zFile)">
+    }else{
+      @ <li>
+    }
+    @ %h(zFile)</a> add by %h(zUser) on
+    hyperlink_to_date(zDate, ".");
+    if( g.okWrWiki && g.okAttach ){
+      @ [<a href="%s(g.zTop)/attachdelete?page=%s(zPageName)&file=%t(zFile)&from=%s(g.zTop)/wiki%%3fname=%s(zPageName)">delete</a>]
+    }
+  }
+  if( cnt ){
+    @ </ul>
+  }
+  db_finalize(&q);
+
   if( !isSandbox ){
     manifest_clear(&m);
   }
   style_footer();
 }
@@ -515,11 +556,13 @@
 /*
 ** Function called to output extra text at the end of each line in
 ** a wiki history listing.
 */
 static void wiki_history_extra(int rid){
-  @ <a href="%s(g.zTop)/wdiff?name=%t(zWikiPageName)&a=%d(rid)">[diff]</a>
+  if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){
+    @ <a href="%s(g.zTop)/wdiff?name=%t(zWikiPageName)&a=%d(rid)">[diff]</a>
+  }
 }
 
 /*
 ** WEBPAGE: whistory
 ** URL: /whistory?name=PAGENAME
@@ -538,13 +581,15 @@
   style_header(zTitle);
   free(zTitle);
 
   zSQL = mprintf("%s AND event.objid IN "
                  "  (SELECT rid FROM tagxref WHERE tagid="
-                       "(SELECT tagid FROM tag WHERE tagname='wiki-%q'))"
+                       "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
+                 "   UNION SELECT attachid FROM attachment"
+                          " WHERE target=%Q)"
                  "ORDER BY mtime DESC",
-                 timeline_query_for_www(), zPageName);
+                 timeline_query_for_www(), zPageName, zPageName);
   db_prepare(&q, zSQL);
   free(zSQL);
   zWikiPageName = zPageName;
   www_print_timeline(&q, TIMELINE_ARTID, wiki_history_extra);
   db_finalize(&q);
@@ -605,25 +650,42 @@
 }
 
 /*
 ** WEBPAGE: wcontent
 **
+**     all=1         Show deleted pages
+**
 ** List all available wiki pages with date created and last modified.
 */
 void wcontent_page(void){
   Stmt q;
+  int showAll = P("all")!=0;
+
   login_check_credentials();
   if( !g.okRdWiki ){ login_needed(); return; }
   style_header("Available Wiki Pages");
+  if( showAll ){
+    style_submenu_element("Active", "Only Active Pages", "%s/wcontent", g.zTop);
+  }else{
+    style_submenu_element("All", "All", "%s/wcontent?all=1", g.zTop);
+  }
   @ <ul>
   db_prepare(&q,
-    "SELECT substr(tagname, 6, 1000) FROM tag WHERE tagname GLOB 'wiki-*'"
+    "SELECT"
+    "  substr(tagname, 6),"
+    "  (SELECT value FROM tagxref WHERE tagid=tag.tagid ORDER BY mtime DESC)"
+    "  FROM tag WHERE tagname GLOB 'wiki-*'"
     " ORDER BY lower(tagname)"
   );
   while( db_step(&q)==SQLITE_ROW ){
     const char *zName = db_column_text(&q, 0);
-    @ <li><a href="%s(g.zBaseURL)/wiki?name=%T(zName)">%h(zName)</a></li>
+    int size = db_column_int(&q, 1);
+    if( size>0 ){
+      @ <li><a href="%s(g.zTop)/wiki?name=%T(zName)">%h(zName)</a></li>
+    }else if( showAll ){
+      @ <li><a href="%s(g.zTop)/wiki?name=%T(zName)"><s>%h(zName)</s></a></li>
+    }
   }
   db_finalize(&q);
   @ </ul>
   style_footer();
 }

Changes to src/wikiformat.c

@@ -369,10 +369,23 @@
     short allowWiki;             /* ALLOW_WIKI if wiki allowed before tag */
     const char *zId;             /* ID attribute or NULL */
   } *aStack;
 };
 
+/*
+** Return TRUE if HTML should be used as the sole markup language for wiki.
+**
+** On first invocation, this routine consults the "wiki-use-html" setting.
+** It caches the result for subsequent invocations, under the assumption
+** that the setting will not change.
+*/
+static int wikiUsesHtml(void){
+  static int r = -1;
+  if( r<0 ) r = db_get_boolean("wiki-use-html", 0);
+  return r;
+}
+
 
 /*
 ** z points to a "<" character.  Check to see if this is the start of
 ** a valid markup.  If it is, return the total number of characters in
 ** the markup including the initial "<" and the terminating ">".  If
@@ -620,11 +633,10 @@
 ** Parse only Wiki links, return everything else as TOKEN_RAW.
 **
 ** z points to the start of a token.  Return the number of
 ** characters in that token. Write the token type into *pTokenType.
 */
-
 static int nextRawToken(const char *z, Renderer *p, int *pTokenType){
   int n;
   if( z[0]=='[' && (n = linkLength(z))>0 ){
     *pTokenType = TOKEN_LINK;
     return n;
@@ -778,11 +790,11 @@
 static void popStack(Renderer *p){
   if( p->nStack ){
     int iCode;
     p->nStack--;
     iCode = p->aStack[p->nStack].iCode;
-    if( iCode!=MARKUP_DIV ){
+    if( iCode!=MARKUP_DIV && p->pOut ){
       blob_appendf(p->pOut, "</%s>", aMarkup[iCode].zName);
     }
   }
 }
 
@@ -1391,11 +1403,11 @@
   if( flags & WIKI_INLINE ){
     renderer.wantAutoParagraph = 0;
   }else{
     renderer.wantAutoParagraph = 1;
   }
-  if( db_get_int("wiki-use-html", 0) ){
+  if( wikiUsesHtml() ){
     renderer.state |= WIKI_USE_HTML;
   }
   if( pOut ){
     renderer.pOut = pOut;
   }else{
@@ -1446,6 +1458,187 @@
   for(i=iStart; z[i] && (z[i]!='<' || strncmp(&z[i],"</title>",8)!=0); i++){}
   if( z[i]!='<' ) return 0;
   blob_init(pTitle, &z[iStart], i-iStart);
   blob_init(pTail, &z[i+8], -1);
   return 1;
+}
+
+/*
+** Parse text looking for wiki hyperlinks in one of the formats:
+**
+**       [target]
+**       [target|...]
+**
+** Where "target" can be either an artifact ID prefix or a wiki page
+** name.  For each such hyperlink found, add an entry to the
+** backlink table.
+*/
+void wiki_extract_links(
+  char *z,           /* The wiki text from which to extract links */
+  int srcid,         /* srcid field for new BACKLINK table entries */
+  int srctype,       /* srctype field for new BACKLINK table entries */
+  double mtime,      /* mtime field for new BACKLINK table entries */
+  int replaceFlag,   /* True first delete prior BACKLINK entries */
+  int flags          /* wiki parsing flags */
+){
+  Renderer renderer;
+  int tokenType;
+  ParsedMarkup markup;
+  int n;
+  int inlineOnly;
+  int wikiUseHtml = 0;
+
+  memset(&renderer, 0, sizeof(renderer));
+  renderer.state = ALLOW_WIKI|AT_NEWLINE|AT_PARAGRAPH;
+  if( flags & WIKI_NOBLOCK ){
+    renderer.state |= INLINE_MARKUP_ONLY;
+  }
+  if( wikiUsesHtml() ){
+    renderer.state |= WIKI_USE_HTML;
+    wikiUseHtml = 1;
+  }
+  inlineOnly = (renderer.state & INLINE_MARKUP_ONLY)!=0;
+  if( replaceFlag ){
+    db_multi_exec("DELETE FROM backlink WHERE srctype=%d AND srcid=%d",
+                  srctype, srcid);
+  }
+
+  while( z[0] ){
+    if( wikiUseHtml ){
+      n = nextRawToken(z, &renderer, &tokenType);
+    }else{
+      n = nextWikiToken(z, &renderer, &tokenType);
+    }
+    switch( tokenType ){
+      case TOKEN_LINK: {
+        char *zTarget;
+        int i, c;
+        char zLink[42];
+
+        zTarget = &z[1];
+        for(i=0; zTarget[i] && zTarget[i]!='|' && zTarget[i]!=']'; i++){}
+        while(i>1 && zTarget[i-1]==' '){ i--; }
+        c = zTarget[i];
+        zTarget[i] = 0;
+        if( is_valid_uuid(zTarget) ){
+          memcpy(zLink, zTarget, i+1);
+          canonical16(zLink, i);
+          db_multi_exec(
+             "REPLACE INTO backlink(target,srctype,srcid,mtime)"
+             "VALUES(%Q,%d,%d,%g)", zLink, srctype, srcid, mtime
+          );
+        }
+        zTarget[i] = c;
+        break;
+      }
+      case TOKEN_MARKUP: {
+        const char *zId;
+        int iDiv;
+        parseMarkup(&markup, z);
+
+        /* Markup of the form </div id=ID> where there is a matching
+        ** ID somewhere on the stack.  Exit the verbatim if were are in
+        ** it.  Pop the stack up to the matching <div>.  Discard the
+        ** </div>
+        */
+        if( markup.iCode==MARKUP_DIV && markup.endTag &&
+             (zId = markupId(&markup))!=0 &&
+             (iDiv = findTagWithId(&renderer, MARKUP_DIV, zId))>=0
+        ){
+          if( renderer.inVerbatim ){
+            renderer.inVerbatim = 0;
+            renderer.state = renderer.preVerbState;
+          }
+          while( renderer.nStack>iDiv+1 ) popStack(&renderer);
+          if( renderer.aStack[iDiv].allowWiki ){
+            renderer.state |= ALLOW_WIKI;
+          }else{
+            renderer.state &= ~ALLOW_WIKI;
+          }
+          renderer.nStack--;
+        }else
+
+        /* If within <verbatim id=ID> ignore everything other than
+        ** </verbatim id=ID> and the </dev id=ID2> above.
+        */
+        if( renderer.inVerbatim ){
+          if( endVerbatim(&renderer, &markup) ){
+            renderer.inVerbatim = 0;
+            renderer.state = renderer.preVerbState;
+          }else{
+            n = 1;
+          }
+        }else
+
+        /* Render invalid markup literally.  The markup appears in the
+        ** final output as plain text.
+        */
+        if( markup.iCode==MARKUP_INVALID ){
+          n = 1;
+        }else
+
+        /* If the markup is not font-change markup ignore it if the
+        ** font-change-only flag is set.
+        */
+        if( (markup.iType&MUTYPE_FONT)==0 &&
+                            (renderer.state & FONT_MARKUP_ONLY)!=0 ){
+          /* Do nothing */
+        }else
+
+        if( markup.iCode==MARKUP_NOWIKI ){
+          if( markup.endTag ){
+            renderer.state |= ALLOW_WIKI;
+          }else{
+            renderer.state &= ~ALLOW_WIKI;
+          }
+        }else
+
+        /* Ignore block markup for in-line rendering.
+        */
+        if( inlineOnly && (markup.iType&MUTYPE_INLINE)==0 ){
+          /* Do nothing */
+        }else
+
+        /* Generate end-tags */
+        if( markup.endTag ){
+          popStackToTag(&renderer, markup.iCode);
+        }else
+
+        /* Push <div> markup onto the stack together with the id=ID attribute.
+        */
+        if( markup.iCode==MARKUP_DIV ){
+          pushStackWithId(&renderer, markup.iCode, markupId(&markup),
+                          (renderer.state & ALLOW_WIKI)!=0);
+        }else
+
+        /* Enter <verbatim> processing.  With verbatim enabled, all other
+        ** markup other than the corresponding end-tag with the same ID is
+        ** ignored.
+        */
+        if( markup.iCode==MARKUP_VERBATIM ){
+          int vAttrIdx, vAttrDidAppend=0;
+          renderer.zVerbatimId = 0;
+          renderer.inVerbatim = 1;
+          renderer.preVerbState = renderer.state;
+          renderer.state &= ~ALLOW_WIKI;
+          for (vAttrIdx = 0; vAttrIdx < markup.nAttr; vAttrIdx++){
+            if( markup.aAttr[vAttrIdx].iACode == ATTR_ID ){
+              renderer.zVerbatimId = markup.aAttr[0].zValue;
+            }else if( markup.aAttr[vAttrIdx].iACode == ATTR_TYPE ){
+              vAttrDidAppend=1;
+            }
+          }
+          renderer.wantAutoParagraph = 0;
+        }
+
+        /* Restore the input text to its original configuration
+        */
+        unparseMarkup(&markup);
+        break;
+      }
+      default: {
+        break;
+      }
+    }
+    z += n;
+  }
 }

Changes to src/winhttp.c

@@ -137,13 +137,13 @@
 ** Start a listening socket and process incoming HTTP requests on
 ** that socket.
 */
 void win32_http_server(
   int mnPort, int mxPort,   /* Range of allowed TCP port numbers */
-  char *zBrowser,           /* Command to launch browser.  (Or NULL) */
-  char *zStopper,           /* Stop server when this file is exists (Or NULL) */
-  char *zNotFound           /* The --notfound option, or NULL */
+  const char *zBrowser,     /* Command to launch browser.  (Or NULL) */
+  const char *zStopper,     /* Stop server when this file is exists (Or NULL) */
+  const char *zNotFound     /* The --notfound option, or NULL */
 ){
   WSADATA wd;
   SOCKET s = INVALID_SOCKET;
   SOCKADDR_IN addr;
   int idCnt = 0;

Changes to src/xfer.c

@@ -518,10 +518,11 @@
   int cnt = 0;
   db_prepare(&q,
     "SELECT uuid FROM unclustered JOIN blob USING(rid)"
     " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)"
     "   AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)"
+    "   AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)"
   );
   while( db_step(&q)==SQLITE_ROW ){
     blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0));
     cnt++;
   }
@@ -536,10 +537,11 @@
   Stmt q;
   db_prepare(&q,
     "SELECT uuid FROM blob "
     " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)"
     "   AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)"
+    "   AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)"
   );
   while( db_step(&q)==SQLITE_ROW ){
     blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0));
   }
   db_finalize(&q);
@@ -1064,10 +1066,11 @@
     nCardSent = 0;
     nCardRcvd = 0;
     xfer.nFileSent = 0;
     xfer.nDeltaSent = 0;
     xfer.nGimmeSent = 0;
+    xfer.nIGotSent = 0;
     fflush(stdout);
     http_exchange(&send, &recv, cloneFlag==0 || nCycle>0);
     blob_reset(&send);
 
     /* Begin constructing the next message (which might never be

Changes to src/zip.c

@@ -378,23 +378,42 @@
   blob_reset(&filename);
   zip_close(pZip);
 }
 
 /*
-** COMMAND: test-baseline-zip
+** COMMAND: zip
+**
+** Usage: %fossil zip VERSION OUTPUTFILE [--name DIRECTORYNAME]
 **
-** Generate a ZIP archive for a specified baseline.
+** Generate a ZIP archive for a specified version.  If the --name option is
+** used, it argument becomes the name of the top-level directory in the
+** resulting ZIP archive.  If --name is omitted, the top-level directory
+** named is derived from the project name, the check-in date and time, and
+** the artifact ID of the check-in.
 */
 void baseline_zip_cmd(void){
   int rid;
   Blob zip;
+  const char *zName;
+  zName = find_option("name", 0, 1);
+  db_find_and_open_repository(1);
   if( g.argc!=4 ){
-    usage("UUID ZIPFILE");
+    usage("VERSION OUTPUTFILE");
   }
-  db_must_be_within_tree();
   rid = name_to_rid(g.argv[2]);
-  zip_of_baseline(rid, &zip, g.argv[2]);
+  if( zName==0 ){
+    zName = db_text("default-name",
+       "SELECT replace(%Q,' ','_') "
+          " || strftime('_%%Y-%%m-%%d_%%H%%M%%S_', event.mtime) "
+          " || substr(blob.uuid, 1, 10)"
+       "  FROM event, blob"
+       " WHERE event.objid=%d"
+       "   AND blob.rid=%d",
+       db_get("project-name", "unnamed"), rid, rid
+    );
+  }
+  zip_of_baseline(rid, &zip, zName);
   blob_write_to_file(&zip, g.argv[3]);
 }
 
 /*
 ** WEBPAGE: zip

Changes to www/branching.wiki

@@ -1,6 +1,6 @@
-<title>Fossil Documentation</title>
+<title>Fossil Documentation</title>
 <h1 align="center">
 Branching, Forking, Merging, and Tagging
 </h1>
 
 In a simple and perfect world, the development of a project would proceed

Added www/build-icons/linux.gif

Added www/build-icons/linux64.gif

Added www/build-icons/mac.gif

Added www/build-icons/src.gif

Added www/build-icons/win32.gif

Changes to www/build.wiki

@@ -34,13 +34,10 @@
 
 <li><p>Select a version of of fossil you want to download.  Click on its
 link.  Note that you must successfully log in as "anonymous" in step 1
 above in order to see the link to the detailed version information.</p></li>
 
-<li><p>On the version information page, click on the "[details]" hyperlink
-that appears right after the check-in comment at the top of the page.</p>
-
 <li><p>Finally, click on the
 "Zip Archive" link.  This link will build a ZIP archive of the
 complete source code and download it to your browser.
 </ol>
 
@@ -54,11 +51,11 @@
 ZIP archive you downloaded into that directory.  You should be
 in the top-level folder of that directory</p></li>
 
 <li><p><b>(Optional:)</b>
 Edit the Makefile to set it up like you want.  You probably do not
-need to do anything.  Do not be intimidated:  There are only 5
+need to do anything.  Do not be intimidated:  There are less than 10
 variables in the makefile that can be changed.  The whole Makefile
 is only a few dozen lines long and most of those lines are comments.</p>
 
 <li><p>Type "<b>make</b>"
 </ol>

Changes to www/embeddeddoc.wiki

@@ -58,12 +58,12 @@
 only works when you start your server using the "<b>fossil server</b>"
 or "<b>fossil ui</b>"
 command line and is intended to show what the documentation you are currently
 editing looks like before you check it in.
 
-Finally, the <i>&lt;filename&gt;</i> element of the URL is the full
-pathname of the documentation file starting from the root of the source
+Finally, the <i>&lt;filename&gt;</i> element of the URL is the
+pathname of the documentation file relative to the root of the source
 tree.
 
 The mimetype (and thus the rendering) of documentation files is
 determined by the file suffix.  Fossil currently understands 192
 different file suffixes, including all the popular ones such as
@@ -110,14 +110,29 @@
 For example, to see the version of this document associated with
 check-in [9be1b00392], simply replace the "<b>/tip/</b>" with
 "<b>/9be1b00392/</b>".  You can also substitute the symbolic name
 for a particular version or branch.  For example, you might
 replace "<b>/tip/</b>" with "<b>/trunk/</b>" to get the latest
-version of this document in the "trunk" branch.  (As of this writing,
-the self-hosting fossil repository only has a single branch "trunk" and
-so "trunk" and "tip" amount to the same thing, but they would be different
-in a project with multiple branches.)
+version of this document in the "trunk" branch.  The symbolic name
+can also be a date and time string in any of the following formats:</p>
+
+<ul>
+<li> <i>YYYY-MM-DD</i>
+<li> <i>YYYY-MM-DD</i><b>T</b><i>HH:MM</i>
+<li> <i>YYYY-MM-DD</i><b>T</b><i>HH:MM:SS</i>
+</ul>
+
+When the symbolic name is a date and time, fossil shows the version
+of the document that was most recently checked in as of the date
+and time specified.  So, for example, to see what the fossil website
+looked like at the beginning of 2010, enter:
+
+<blockquote>
+<a href="http://www.fossil-scm.org/index.html/doc/2010-01-01/www/index.wiki">
+http://www.fossil-scm.org/index.html/doc/<b>2010-01-01</b>/www/index.wiki
+</a>
+</blockquote>
 
 The file that encodes this document is stored in the fossil source tree under
 the name "<b>www/embeddeddoc.wiki</b>" and so that name forms the
 last part of the URL for this document.
 

Changes to www/fileformat.wiki

@@ -1,26 +1,23 @@
 <title>Fossil File Formats</title>
 <h1 align="center">
 Fossil File Formats
 </h1>
 
-<p>The global state of a fossil repository is kept simple so that it can
+The global state of a fossil repository is kept simple so that it can
 endure in useful form for decades or centuries.
 A fossil repository is intended to be readable,
-searchable, and extensible by people not yet born.</p>
+searchable, and extensible by people not yet born.
 
-<p>
 The global state of a fossil repository is an unordered
 set of <i>artifacts</i>.
 An artifact might be a source code file, the text of a wiki page,
 part of a trouble ticket, or one of several special control artifacts
 used to show the relationships between other artifacts within the
 project.  Each artifact is normally represented on disk as a separate
 file.  Artifacts can be text or binary.
-</p>
 
-<p>
 In addition to the global state,
 each fossil repository also contains local state.
 The local state consists of web-page formatting
 preferences, authorized users, ticket display and reporting formats,
 and so forth.  The global state is shared in common among all
@@ -29,82 +26,73 @@
 The local state is not versioned and is not synchronized
 with the global state.
 The local state is not composed of artifacts and is not intended to be enduring.
 This document is concerned with global state only.  Local state is only
 mentioned here in order to distinguish it from global state.
-</p>
 
-<p>
 Each artifact in the repository is named by its SHA1 hash.
 No prefixes or meta information is added to a artifact before
 its hash is computed.  The name of a artifact in the repository
 is exactly the same SHA1 hash that is computed by sha1sum
 on the file as it exists in your source tree.</p>
 
-<p>
 Some artifacts have a particular format which gives them special
-meaning to fossil.  Fossil recognizes:</p>
+meaning to fossil.  Fossil recognizes:
 
 <ul>
-<li> Manifests </li>
-<li> Clusters </li>
-<li> Control Artifacts </li>
-<li> Wiki Pages </li>
-<li> Ticket Changes </li>
+<li> [#manifest | Manifests] </li>
+<li> [#cluster | Clusters] </li>
+<li> [#ctrl | Control Artifacts] </li>
+<li> [#wikichng | Wiki Pages] </li>
+<li> [#tktchng | Ticket Changes] </li>
+<li> [#artifact | Artifacts] </li>
 </ul>
 
-<p>These five artifact types are described in the sequel.</p>
+These five artifact types are described in the sequel.
 
-<p>In the current implementation (as of 2009-01-25) the artifacts that
+In the current implementation (as of 2009-01-25) the artifacts that
 make up a fossil repository are stored in in as delta- and zlib-compressed
 blobs in an <a href="http://www.sqlite.org/">SQLite</a> database.  This
 is an implementation detail and might change in a future release.  For
 the purpose of this article "file format" means the format of the artifacts,
 not how the artifacts are stored on disk.  It is the artifact format that
 is intended to be enduring.  The specifics of how artifacts are stored on
 disk, though stable, is not intended to live as long as the
-artifact format.</p>
+artifact format.
 
+<a name="manifest"></a>
 <h2>1.0 The Manifest</h2>
 
-<p>A manifest defines a check-in or version of the project
+A manifest defines a check-in or version of the project
 source tree.  The manifest contains a list of artifacts for
 each file in the project and the corresponding filenames, as
 well as information such as parent check-ins, the name of the
 programmer who created the check-in, the date and time when
 the check-in was created, and any check-in comments associated
-with the check-in.</p>
+with the check-in.
 
-<p>
 Any artifact in the repository that follows the syntactic rules
 of a manifest is a manifest.  Note that a manifest can
 be both a real manifest and also a content file, though this
 is rare.
-</p>
 
-<p>
 A manifest is a text file.  Newline characters
 (ASCII 0x0a) separate the file into "cards".
 Each card begins with a single
 character "card type".  Zero or more arguments may follow
 the card type.  All arguments are separated from each other
 and from the card-type character by a single space
 character.  There is no surplus white space between arguments
 and no leading or trailing whitespace except for the newline
 character that acts as the card separator.
-</p>
 
-<p>
 All cards of the manifest occur in strict sorted lexicographical order.
 No card may be duplicated.
 The entire manifest may be PGP clear-signed, but otherwise it
 may contain no additional text or data beyond what is described here.
-</p>
 
-<p>
 Allowed cards in the manifest are as follows:
-</p>
 
 <blockquote>
 <b>C</b> <i>checkin-comment</i><br>
 <b>D</b> <i>time-and-date-stamp</i><br>
 <b>F</b> <i>filename</i> <i>SHA1-hash</i> <i>permissions</i> <i>old-name</i><br>
@@ -113,11 +101,10 @@
 <b>T</b> (<b>+</b>|<b>-</b>|<b>*</b>)<i>tag-name  <b>*</b> ?value?</i><br>
 <b>U</b> <i>user-login</i><br>
 <b>Z</b> <i>manifest-checksum</i>
 </blockquote>
 
-<p>
 A manifest must have exactly one C-card.  The sole argument to
 the C-card is a check-in comment that describes the check-in that
 the manifest defines.  The check-in comment is text.  The following
 escape sequences are applied to the text:
 A space (ASCII 0x20) is represented as "\s" (ASCII 0x5C, 0x73).  A
@@ -124,24 +111,20 @@
 newline (ASCII 0x0a) is "\n" (ASCII 0x6C, x6E).  A backslash
 (ASCII 0x5C) is represented as two backslashes "\\".  Apart from
 space and newline, no other whitespace characters are allowed in
 the check-in comment.  Nor are any unprintable characters allowed
 in the comment.
-</p>
 
-<p>
 A manifest must have exactly one D-card.  The sole argument to
 the D-card is a date-time stamp in the ISO8601 format.  The
 date and time should be in coordinated universal time (UTC).
 The format is:
-</p>
 
 <blockquote>
 <i>YYYY</i><b>-</b><i>MM</i><b>-</b><i>DD</i><b>T</b><i>HH</i><b>:</b><i>MM</i><b>:</b><i>SS</i>
 </blockquote>
 
-<p>
 A manifest has zero or more F-cards.  Each F-card defines a file
 (other than the manifest itself) which is part of the check-in that
 the manifest defines.  There are two, three, or four arguments.
 The first argument
 is the pathname of the file in the check-in relative to the root
@@ -157,13 +140,11 @@
 always readable and writable.  This can be expressed by "w" permission
 if desired but is optional.
 The optional 4th argument is the name of the same file as it existed in
 the parent check-in.  If the name of the file is unchanged from its
 parent, then the 4th argument is omitted.
-</p>
 
-<p>
 A manifest has zero or one P-cards.  Most manifests have one P-card.
 The P-card has a varying number of arguments that
 defines other manifests from which the current manifest
 is derived.  Each argument is an 40-character lowercase
 hexadecimal SHA1 of the predecessor manifest.  All arguments
@@ -171,13 +152,11 @@
 The first predecessor is the direct ancestor of the manifest.
 Other arguments define manifests with which the first was
 merged to yield the current manifest.  Most manifests have
 a P-card with a single argument.  The first manifest in the
 project has no ancestors and thus has no P-card.
-</p>
 
-<p>
 A manifest may optionally have a single R-card.  The R-card has
 a single argument which is the MD5 checksum of all files in
 the check-in except the manifest itself.  The checksum is expressed
 as 32-characters of lowercase hexadecimal.   The checksum is
 computed as follows:  For each file in the check-in (except for
@@ -185,13 +164,11 @@
 take the pathname of the file relative to the root of the
 repository, append a single space (ASCII 0x20), the
 size of the file in ASCII decimal, a single newline
 character (ASCII 0x0A), and the complete text of the file.
 Compute the MD5 checksum of the the result.
-</p>
 
-<p>
 A manifest might contain one or more T-cards used to set tags or
 properties on the check-in.  The format of the T-card is the same as
 described in <i>Control Artifacts</i> section below, except that the
 second argument is the single characcter "<b>*</b>" instead of an
 artifact ID.  The <b>*</b> in place of the artifact ID indicates that
@@ -199,42 +176,35 @@
 possible to encode the current artifact ID as part of an artifact,
 since the act of inserting the artifact ID would change the artifact ID,
 hence a <b>*</b> is used to represent "self".  T-cards are typically
 added to manifests in order to set the <b>branch</b> property and a
 symbolic name when the check-in is intended to start a new branch.
-</p>
 
-<p>
 Each manifest has a single U-card.  The argument to the U-card is
 the login of the user who created the manifest.  The login name
 is encoded using the same character escapes as is used for the
 check-in comment argument to the C-card.
-</p>
 
-<p>
 A manifest has an option Z-card as its last line.  The argument
 to the Z-card is a 32-character lowercase hexadecimal MD5 hash
 of all prior lines of the manifest up to and including the newline
 character that immediately precedes the "Z".  The Z-card is just
 a sanity check to prove that the manifest is well-formed and
 consistent.
-</p>
 
-<p>A sample manifest from Fossil itself can be seen
+A sample manifest from Fossil itself can be seen
 [/artifact/28987096ac | here].
 
+<a name="cluster"></a>
 <h2>2.0 Clusters</h2>
 
-<p>
 A cluster is a artifact that declares the existence of other artifacts.
 Clusters are used during repository synchronization to help
 reduce network traffic.  As such, clusters are an optimization and
 may be removed from a repository without loss or damage to the
 underlying project code.
-</p>
 
-<p>
 Clusters follow a syntax that is very similar to manifests.
 A Cluster is a line-oriented text file.  Newline characters
 (ASCII 0x0a) separate the artifact into cards.  Each card begins with a single
 character "card type".  Zero or more arguments may follow
 the card type.  All arguments are separated from each other
@@ -245,67 +215,58 @@
 All cards of a cluster occur in strict sorted lexicographical order.
 No card may be duplicated.
 The cluster may not contain additional text or data beyond
 what is described here.
 Unlike manifests, clusters are never PGP signed.
-</p>
 
-<p>
 Allowed cards in the cluster are as follows:
-</p>
 
 <blockquote>
 <b>M</b> <i>artifact-id</i><br />
 <b>Z</b> <i>checksum</i>
 </blockquote>
 
-<p>
 A cluster contains one or more "M" cards followed by a single "Z"
 line.  Each M card has a single argument which is the artifact ID of
 another artifact in the repository.  The Z card work exactly like
 the Z card of a manifest.  The argument to the Z card is the
 lower-case hexadecimal representation of the MD5 checksum of all
 prior cards in the cluster.  Note that the Z card is required
 on a cluster.
-</p>
 
-<p>An example cluster from Fossil can be seen
-[/artifact/d03dbdd73a2a8 | here].</p>
+An example cluster from Fossil can be seen
+[/artifact/d03dbdd73a2a8 | here].
 
-
+<a name="ctrl"></a>
 <h2>3.0 Control Artifacts</h2>
 
-<p>
 Control artifacts are used to assign properties to other artifacts
 within the repository.  The basic format of a control artifact is
 the same as a manifest or cluster.  A control artifact is a text
 files divided into cards by newline characters.  Each card has a
 single-character card type followed by arguments.  Spaces separate
 the card type and the arguments.  No surplus whitespace is allowed.
 All cards must occur in strict lexigraphical order.
-</p>
 
-<p>
 Allowed cards in a control artifact are as follows:
-</p>
 
 <blockquote>
 <b>D</b> <i>time-and-date-stamp</i><br />
 <b>T</b> (<b>+</b>|<b>-</b>|<b>*</b>)<i>tag-name  artifact-id  ?value?</i><br />
+<b>U</b> <i>user-name</i><br />
 <b>Z</b> <i>checksum</i><br />
 </blockquote>
 
-<p>
 A control artifact must have one D card and one Z card and
 one or more T cards.  No other cards or other text is
 allowed in a control artifact.  Control artifacts might be PGP
-clearsigned.</p>
+clearsigned.
 
-<p>The D card and the Z card of a control artifact are the same
-as in a manifest.</p>
+The D card and the Z card of a control artifact are the same
+as in a manifest.
 
-<p>The T card represents a "tag" or property that is applied to
+The T card represents a "tag" or property that is applied to
 some other artifact.  The T card has two or three values.  The
 second argument is the 40 character lowercase artifact ID of the artifact
 to which the tag is to be applied. The
 first value is the tag name.  The first character of the tag
 is either "+", "-", or "*".  A "+" means the tag should be added
@@ -313,28 +274,37 @@
 The "*" character means the tag should be added to the artifact
 and all direct descendants (but not branches) of the artifact down
 to but not including the first descendant that contains a
 more recent "-" tag with the same name.
 The optional third argument is the value of the tag.  A tag
-without a value is a boolean.</p>
+without a value is a boolean.
 
-<p>When two or more tags with the same name are applied to the
+When two or more tags with the same name are applied to the
 same artifact, the tag with the latest (most recent) date is
-used.</p>
+used.
 
-<p>Some tags have special meaning.  The "comment" tag when applied
+Some tags have special meaning.  The "comment" tag when applied
 to a check-in will override the check-in comment of that check-in
-for display purposes.</p>
+for display purposes.  The "user" tag overrides the name of the
+check-in user.  The "date" tag overrides the check-in date.
+The "branch" tag sets the name of the branch that at check-in
+belongs to.  Symbolic tags begin with the "sym-" prefix.
+
+The U card is the name of the user that created the control
+artifact.  The Z card is the usual artifact checksum.
+
+An example control artifacts can be seen [/info/9d302ccda8 | here].
+
 
 <a name="wikichng"></a>
 <h2>4.0 Wiki Pages</h2>
 
-<p>A wiki page is an artifact with a format similar to manifests,
+A wiki page is an artifact with a format similar to manifests,
 clusters, and control artifacts.  The artifact is divided into
 cards by newline characters.  The format of each card is as in
 manifests, clusters, and control artifacts.  Wiki artifacts accept
-the following card types:</p>
+the following card types:
 
 <blockquote>
 <b>D</b> <i>time-and-date-stamp</i><br />
 <b>L</b> <i>wiki-title</i><br />
 <b>P</b> <i>parent-artifact-id</i>+<br />
@@ -341,63 +311,245 @@
 <b>U</b> <i>user-name</i><br />
 <b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b><br />
 <b>Z</b> <i>checksum</i>
 </blockquote>
 
-<p>The D card is the date and time when the wiki page was edited.
+The D card is the date and time when the wiki page was edited.
 The P card specifies the parent wiki pages, if any.  The L card
 gives the name of the wiki page.  The U card specifies the login
 of the user who made this edit to the wiki page.  The Z card is
-the usual checksum over the either artifact.</p>
+the usual checksum over the either artifact.
 
-<p>The W card is used to specify the text of the wiki page.  The
+The W card is used to specify the text of the wiki page.  The
 argument to the W card is an integer which is the number of bytes
 of text in the wiki page.  That text follows the newline character
 that terminates the W card.  The wiki text is always followed by one
-extra newline.</p>
+extra newline.
+
+An example wiki artifact can be seen
+[/artifact/7b2f5fd0e0 | here].
 
 <a name="tktchng"></a>
 <h2>5.0 Ticket Changes</h2>
 
-<p>A ticket-change artifact represents a change to a trouble ticket.
-The following cards are allowed on a ticket change artifact:</p>
+A ticket-change artifact represents a change to a trouble ticket.
+The following cards are allowed on a ticket change artifact:
 
 <blockquote>
 <b>D</b> <i>time-and-date-stamp</i><br />
 <b>J</b> ?<b>+</b>?<i>name</i> ?<i>value</i>?<br />
 <b>K</b> <i>ticket-id</i><br />
 <b>U</b> <i>user-name</i><br />
 <b>Z</b> <i>checksum</i>
 </blockquote>
 
-<p>
 The D card is the usual date and time stamp and represents the point
 in time when the change was entered.  The U card is the login of the
 programmer who entered this change.  The Z card is the checksum over
-the entire artifact.</p>
+the entire artifact.
 
-<p>
 Every ticket has a unique ID.  The ticket to which this change is applied
 is specified by the K card.  A ticket exists if it contains one or
 more changes.  The first "change" to a ticket is what brings the
-ticket into existence.</p>
+ticket into existence.
 
-<p>
 J cards specify changes to the "value" of "fields" in the ticket.
 If the <i>value</i> parameter of the J card is omitted, then the
 field is set to an empty string.
 Each fossil server has a ticket configuration which specifies the fields its
 understands.  The ticket configuration is part of the local state for
 the repository and thus can vary from one repository to another.
 Hence a J card might specify a <i>field</i> that do not exist in the
 local ticket configuration.  If a J card specifies a <i>field</i> that
 is not in the local configuration, then that J card
-is simply ignored.</p>
+is simply ignored.
 
-<p>
 The first argument of the J card is the field name.  The second
 value is the field value.  If the field name begins with "+" then
 the value is appended to the prior value.  Otherwise, the value
 on the J card replaces any previous value of the field.
 The field name and value are both encoded using the character
 escapes defined for the C card of a manifest.
-</p>
+
+An example ticket-change artifact can be seen
+[/artifact/91f1ec6af053 | here].
+
+<a name="attachment"></a>
+<h2>6.0 Attachments</h2>
+
+An attachment artifact associates some other artifact that is the
+attachment (the source artifact) with a ticket or wiki page to which
+the attachment is connected (the target artifact).
+The following cards are allowed on an attachment artifact:
+
+<blockquote>
+<b>A</b> <i>filename target</i> ?<i>source</i>?
+<b>C</b> <i>comment</i><br>
+<b>D</b> <i>time-and-date-stamp</i><br />
+<b>U</b> <i>user-name</i><br />
+<b>Z</b> <i>checksum</i>
+</blockquote>
+
+The A card specifies a filename for the attachment in its first argument.
+The second argument to the A card is the name
+of the wiki page or ticket to which the attachment is connected.  The
+third argument is either missing or else it is the 40-character artifact
+ID of the attachment itself.  A missing third argument means that the
+attachment should be deleted.
+
+The C card is an optional comment describing what the attachment is about.
+The C card is optional, but there can only be one.
+
+A single D card is required to give the date and time when the attachment
+was applied.
+
+A single U card gives the name of the user to added the attachment.
+If an attachment is added anonymously, then the U card may be omitted.
+
+The Z card is the usual checksum over the rest of the attachment artifact.
+
+
+<a name="summary"></a>
+<h2>7.0 Card Summary</h2>
+
+The following table summaries the various kinds of cards that
+appear on Fossil artifacts:
+
+<table border=1 width="100%">
+<tr>
+<th rowspan=2 valign=bottom>Card Format</th>
+<th colspan=6>Used By</th>
+</tr>
+<tr>
+<th>Manifest</th>
+<th>Cluster</th>
+<th>Control</th>
+<th>Wiki</th>
+<th>Ticket</th>
+<th>Attachment</th>
+</tr>
+<tr>
+<td><b>A</b> <i>filename target source</i></td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td align=center><b>X</b></td>
+</tr>
+<tr>
+<td><b>C</b> <i>coment-text</i></td>
+<td align=center><b>X</b></td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td>&nbsp;</td>
+<td align=center><b>X</b></td>
+</tr>
+<tr>
+<td><b>D</b> <i>date-time-stamp</i></td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+</tr>
+<tr>
+<td><b>F</b> <i>filename uuid permissions oldname</i></td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>J</b> <i>name value</i></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>K</b> <i>ticket-uuid</i></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>L</b> <i>wiki-title</i></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>M</b> <i>uuid</i></td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>P</b> <i>uuid ...</i></td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>R</b> <i>md5sum</i></td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<tr>
+<td><b>T</b> (<b>+</b>|<b>*</b>|<b>-</b>)<i>tagname uuid value</i></td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>U</b> <i>username</i></td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+</tr>
+<tr>
+<td><b>W</b> <i>size</i></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+<td align=center><b>X</b></td>
+<td align=center>&nbsp;</td>
+<td align=center>&nbsp;</td>
+</tr>
+<tr>
+<td><b>Z</b> <i>md5sum</i></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+<td align=center><b>X</b></td>
+</tr>
+</table>

Changes to www/mkdownload.tcl

@@ -27,54 +27,73 @@
 The historical source code is also available in the
 <a href="/fossil/doc/tip/www/selfhost.wiki">self-hosting
 Fossil repositories</a>.
 </p>
 
-<table cellpadding="5">
+<p>
+<u>Important Note:</u>
+After upgrading to a newer version of fossil, it is always a good idea
+to run:
+<blockquote><pre>
+<b><big><tt>fossil all rebuild</tt></big></b>
+</pre></blockquote>
+Running "rebuild" this way is not always necessary, but it never hurts.
+</p>
+
+<table cellpadding="10">
 }
 
-proc Product {pattern desc} {
-  set flist [glob -nocomplain download/$pattern]
-  foreach file [lsort -dict $flist] {
-    set file [file tail $file]
-    if {![regexp -- {-([0-9]+)\.} $file all version]} continue
-    set mtime [file mtime download/$file]
-    set date [clock format $mtime -format {%Y-%m-%d %H:%M:%S UTC} -gmt 1]
-    set size [file size download/$file]
-    set units bytes
-    if {$size>1024*1024} {
-      set size [format %.2f [expr {$size/(1024.0*1024.0)}]]
-      set units MiB
-    } elseif {$size>1024} {
-      set size [format %.2f [expr {$size/(1024.0)}]]
-      set units KiB
-    }
-    puts "<tr><td width=\"10\"></td>"
-    puts "<td valign=\"top\" align=\"right\">"
-    puts "<a href=\"download/$file\">$file</a></td>"
-    puts "<td width=\"5\"></td>"
-    regsub -all VERSION $desc $version d2
-    puts "<td valign=\"top\">[string trim $d2].<br>Size: $size $units.<br>"
-    puts "Created: $date</td></tr>"
+# Find all all unique timestamps.
+#
+foreach file [glob -nocomplain download/fossil-*.zip] {
+  if {[regexp {(\d+).zip$} $file all datetime]
+       && [string length $datetime]==14} {
+    set adate($datetime) 1
   }
 }
+
+# Do all dates from newest to oldest
+#
+foreach datetime [lsort -decr [array names adate]] {
+  set dt [string range $datetime 0 3]-[string range $datetime 4 5]-
+  append dt "[string range $datetime 6 7] "
+  append dt "[string range $datetime 8 9]:[string range $datetime 10 11]:"
+  append dt "[string range $datetime 12 13]"
+  set link [string map {{ } +} $dt]
+  set hr http://www.fossil-scm.org/fossil/timeline?c=$link&y=ci
+  puts "<tr><td colspan=5 align=center><hr>"
+  puts "<b>Fossil snapshot as of <a href=\"$hr\">$dt</a><td width=30></b>"
+  puts "</td></tr>"
 
-Product fossil-linux-x86-*.zip {
-  Prebuilt fossil binary version [VERSION] for Linux on x86
-}
-Product fossil-linux-amd64-*.zip {
-  Prebuilt fossil binary version [VERSION] for Linux on amd64
-}
-Product fossil-macosx-x86-*.zip {
-  Prebuilt fossil binary version [VERSION] for MacOSX on x86
+  foreach {prefix suffix img desc} {
+    fossil-linux-x86 zip linux.gif {Linux x86}
+    fossil-linux-amd64 zip linux64.gif {Linux x86_64}
+    fossil-macosx-x86 zip mac.gif {Mac 10.5 x86}
+    fossil-w32 zip win32.gif {Windows}
+    fossil-src tar.gz src.gif {Source Tarball}
+  } {
+    set filename download/$prefix-$datetime.$suffix
+    if {[file exists $filename]} {
+      set size [file size $filename]
+      set units bytes
+      if {$size>1024*1024} {
+        set size [format %.2f [expr {$size/(1024.0*1024.0)}]]
+        set units MiB
+      } elseif {$size>1024} {
+        set size [format %.2f [expr {$size/(1024.0)}]]
+        set units KiB
+      }
+      puts "<td align=center valign=bottom><a href=\"$filename\">"
+      puts "<img src=\"build-icons/$img\" border=0><br>$desc</a><br>"
+      puts "$size $units</td>"
+    } else {
+      puts "<td>&nbsp;</td>"
+    }
+  }
+  puts "</tr>"
 }
-Product fossil-w32-*.zip {
-  Prebuilt fossil binary version [VERSION] for windows
-}
-Product fossil-src-*.tar.gz {
-  Source code tarball for fossil version [VERSION]
-}
+puts "<tr><td colspan=5><hr></td></tr>"
 
 puts {</table>
 </body>
 </html>
 }

Changes to www/quickstart.wiki

@@ -67,18 +67,19 @@
 
     <blockquote>
     <b>fossil ui </b><i> repository-filename</i>
     </blockquote>
 
-    <p>This starts a webserver listening on port 8080.  You can
-    specify a different port using the <b>-port</b> option on the command-line.
-    After the server is running, fossil will then attempt to launch your
-    web browser and make it point to this web server.  If your system
+    <p>This starts a web server then automatically launches your
+    web browser and makes it point to this web server.  If your system
     has an unusual configuration, fossil might not be able to figure out
-    how to start your web browser.  In that case, start the web browser
-    yourself and point it at http://localhost:8080/.  Click on the
-    "Admin" link on the menu bar to start configuring your repository.</p>
+    how to start your web browser.  In that case, first tell fossil
+    where to find your web browser using a command like this:</p>
+
+    <blockquote>
+    <b>fossil setting web-browser </b><i>  path-to-web-browser</i>
+    </blockquote>
 
     <p>By default, fossil does not require a login for HTTP connections
     coming in from the IP loopback address 127.0.0.1.  You can, and perhaps
     should, change this after you create a few users.</p>
 

Changes to www/webui.wiki

@@ -148,18 +148,5 @@
   </verbatim>
 
 As always, you'll want to adjust the pathnames to whatever is appropriate
 for your system.  The xinetd setup uses a different syntax but follows
 the same idea.
-
-Once you have your new repository running on the network server, delete
-the original repository from your local machine, then clone the repository
-off of the server:
-
-  <b>fossil clone http://user:password@myserver.org/cgi-bin/my-project</b>
-
-(As always, adjust the URL as appropriate for your installation.)
-After copying a repository, it is important to reclone it onto new machines.
-Each repository has a random "repository ID" and repositories will not
-sync with another repository having the same ID (to avoid sync loops).
-Cloning the repository will give you a new repository ID in your local
-copy and allow you to sync with the server.

Changes to www/wikitheory.wiki

@@ -8,11 +8,11 @@
    *  [./embeddeddoc.wiki | Embedded documentation] files whose
       name ends in "wiki".
 
 The [/wiki_rules | formatting rules] for fossil wiki
 are designed to be simple and intuitive.  The idea is that wiki provides
-paragaph breaks, numbered and bulletted lists, and hyperlinking for
+paragraph breaks, numbered and bulletted lists, and hyperlinking for
 simple documents together with a safe subset of HTML for more complex
 formatting tasks.
 
 Some commentators feel that the use of HTML is a mistake and that
 fossil should use the markup language of the