Index: src/checkin.c
==================================================================
--- src/checkin.c
+++ src/checkin.c
@@ -276,15 +276,15 @@
   int dotfilesFlag;
   const char *zIgnoreFlag;
   Blob path, repo;
   Stmt q;
   int n;
+  Glob *pIgnore;
+
   allFlag = find_option("force","f",0)!=0;
   dotfilesFlag = find_option("dotfiles",0,0)!=0;
   zIgnoreFlag = find_option("ignore",0,1);
-  Glob *pIgnore;
-
   db_must_be_within_tree();
   if( zIgnoreFlag==0 ){
     zIgnoreFlag = db_get("ignore-glob", 0);
   }
   db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)");

Index: src/clone.c
==================================================================
--- src/clone.c
+++ src/clone.c
@@ -64,14 +64,14 @@
     file_copy(g.urlName, g.argv[3]);
     db_close(1);
     db_open_repository(g.argv[3]);
     db_record_repository_filename(g.argv[3]);
     db_multi_exec(
-      "REPLACE INTO config(name,value)"
-      " VALUES('server-code', lower(hex(randomblob(20))));"
-      "REPLACE INTO config(name,value)"
-      " VALUES('last-sync-url', '%q');",
+      "REPLACE INTO config(name,value,mtime)"
+      " VALUES('server-code', lower(hex(randomblob(20))),now());"
+      "REPLACE INTO config(name,value,mtime)"
+      " VALUES('last-sync-url', '%q',now());",
       g.urlCanonical
     );
     db_multi_exec(
        "DELETE FROM blob WHERE rid IN private;"
        "DELETE FROM delta wHERE rid IN private;"
@@ -92,12 +92,12 @@
     user_select();
     db_set("content-schema", CONTENT_SCHEMA, 0);
     db_set("aux-schema", AUX_SCHEMA, 0);
     db_set("last-sync-url", g.argv[2], 0);
     db_multi_exec(
-      "REPLACE INTO config(name,value)"
-      " VALUES('server-code', lower(hex(randomblob(20))));"
+      "REPLACE INTO config(name,value,mtime)"
+      " VALUES('server-code', lower(hex(randomblob(20))), now());"
     );
     url_enable_proxy(0);
     url_get_password_if_needed();
     g.xlinkClusterOnly = 1;
     nErr = client_sync(0,0,1,bPrivate,CONFIGSET_ALL,0);

Index: src/configure.c
==================================================================
--- src/configure.c
+++ src/configure.c
@@ -2,11 +2,11 @@
 ** Copyright (c) 2008 D. Richard Hipp
 **
 ** This program is free software; you can redistribute it and/or
 ** modify it under the terms of the Simplified BSD License (also
 ** known as the "2-Clause License" or "FreeBSD License".)
-
+**
 ** This program is distributed in the hope that it will be useful,
 ** but without any warranty; without even the implied warranty of
 ** merchantability or fitness for a particular purpose.
 **
 ** Author contact information:
@@ -27,18 +27,21 @@
 #if INTERFACE
 /*
 ** Configuration transfers occur in groups.  These are the allowed
 ** groupings:
 */
-#define CONFIGSET_SKIN   0x000001     /* WWW interface appearance */
-#define CONFIGSET_TKT    0x000002     /* Ticket configuration */
-#define CONFIGSET_PROJ   0x000004     /* Project name */
-#define CONFIGSET_SHUN   0x000008     /* Shun settings */
-#define CONFIGSET_USER   0x000010     /* The USER table */
-#define CONFIGSET_ADDR   0x000020     /* The CONCEALED table */
+#define CONFIGSET_SKIN      0x000001     /* WWW interface appearance */
+#define CONFIGSET_TKT       0x000002     /* Ticket configuration */
+#define CONFIGSET_PROJ      0x000004     /* Project name */
+#define CONFIGSET_SHUN      0x000008     /* Shun settings */
+#define CONFIGSET_USER      0x000010     /* The USER table */
+#define CONFIGSET_ADDR      0x000020     /* The CONCEALED table */
 
-#define CONFIGSET_ALL    0xffffff     /* Everything */
+#define CONFIGSET_ALL       0x0000ff     /* Everything */
+
+#define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
+#define CONFIGSET_OLDFORMAT 0x200000     /* Use the legacy format */
 
 #endif /* INTERFACE */
 
 /*
 ** Names of the configuration sets
@@ -46,17 +49,17 @@
 static struct {
   const char *zName;   /* Name of the configuration set */
   int groupMask;       /* Mask for that configuration set */
   const char *zHelp;   /* What it does */
 } aGroupName[] = {
-  { "email",        CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
-  { "project",      CONFIGSET_PROJ,  "Project name and description"         },
-  { "skin",         CONFIGSET_SKIN,  "Web interface apparance settings"     },
-  { "shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
-  { "ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
-  { "user",         CONFIGSET_USER,  "Users and privilege settings"         },
-  { "all",          CONFIGSET_ALL,   "All of the above"                     },
+  { "/email",        CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
+  { "/project",      CONFIGSET_PROJ,  "Project name and description"         },
+  { "/skin",         CONFIGSET_SKIN,  "Web interface apparance settings"     },
+  { "/shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
+  { "/ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
+  { "/user",         CONFIGSET_USER,  "Users and privilege settings"         },
+  { "/all",          CONFIGSET_ALL,   "All of the above"                     },
 };
 
 
 /*
 ** The following is a list of settings that we are willing to
@@ -106,15 +109,29 @@
 const char *configure_first_name(int iMask){
   iConfig = 0;
   return configure_next_name(iMask);
 }
 const char *configure_next_name(int iMask){
-  while( iConfig<count(aConfig) ){
-    if( aConfig[iConfig].groupMask & iMask ){
-      return aConfig[iConfig++].zName;
-    }else{
-      iConfig++;
+  if( iMask & CONFIGSET_OLDFORMAT ){
+    while( iConfig<count(aConfig) ){
+      if( aConfig[iConfig].groupMask & iMask ){
+        return aConfig[iConfig++].zName;
+      }else{
+        iConfig++;
+      }
+    }
+  }else{
+    if( iConfig==0 && (iMask & CONFIGSET_ALL)==CONFIGSET_ALL ){
+      iConfig = count(aGroupName);
+      return "/all";
+    }
+    while( iConfig<count(aGroupName)-1 ){
+      if( aGroupName[iConfig].groupMask & iMask ){
+        return aGroupName[iConfig++].zName;
+      }else{
+        iConfig++;
+      }
     }
   }
   return 0;
 }
 
@@ -127,12 +144,17 @@
 ** login credentials and has sufficient capabilities to access the requested
 ** information.
 */
 int configure_is_exportable(const char *zName){
   int i;
+  int n = strlen(zName);
+  if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){
+    zName++;
+    n -= 2;
+  }
   for(i=0; i<count(aConfig); i++){
-    if( fossil_strcmp(zName, aConfig[i].zName)==0 ){
+    if( memcmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
       int m = aConfig[i].groupMask;
       if( !g.okAdmin ){
         m &= ~CONFIGSET_USER;
       }
       if( !g.okRdAddr ){
@@ -200,38 +222,39 @@
 }
 
 /*
 ** Two SQL functions:
 **
-**        flag_test(int)
-**        flag_clear(int)
+**        config_is_reset(int)
+**        config_reset(int)
 **
-** The flag_test() function takes the integer valued argument and
-** ANDs it against the static variable "flag_value" below.  The
-** function returns TRUE or false depending on the result.  The
-** flag_clear() function masks off the bits from "flag_value" that
+** The config_is_reset() function takes the integer valued argument and
+** ANDs it against the static variable "configHasBeenReset" below.  The
+** function returns TRUE or FALSE depending on the result depending on 
+** whether or not the corresponding configuration table has been reset.  The
+** config_reset() function adds the bits to "configHasBeenReset" that
 ** are given in the argument.
 **
 ** These functions are used below in the WHEN clause of a trigger to
 ** get the trigger to fire exactly once.
 */
-static int flag_value = 0xffff;
-static void flag_test_function(
+static int configHasBeenReset = 0;
+static void config_is_reset_function(
   sqlite3_context *context,
   int argc,
   sqlite3_value **argv
 ){
   int m = sqlite3_value_int(argv[0]);
-  sqlite3_result_int(context, (flag_value&m)!=0 );
+  sqlite3_result_int(context, (configHasBeenReset&m)!=0 );
 }
-static void flag_clear_function(
+static void config_reset_function(
   sqlite3_context *context,
   int argc,
   sqlite3_value **argv
 ){
   int m = sqlite3_value_int(argv[0]);
-  flag_value &= ~m;
+  configHasBeenReset |= m;
 }
 
 /*
 ** Create the temporary _xfer_reportfmt and _xfer_user tables that are
 ** necessary in order to evalute the SQL text generated by the
@@ -261,12 +284,14 @@
     @   ipaddr TEXT,                    -- IP address for which cookie is valid
     @   cexpire DATETIME,               -- Time when cookie expires
     @   info TEXT,                      -- contact information
     @   photo BLOB                      -- JPEG image of this user
     @ );
-    @ INSERT INTO _xfer_reportfmt SELECT * FROM reportfmt;
-    @ INSERT INTO _xfer_user SELECT * FROM user;
+    @ INSERT INTO _xfer_reportfmt
+    @    SELECT rn,owner,title,cols,sqlcode FROM reportfmt;
+    @ INSERT INTO _xfer_user
+    @    SELECT uid,login,pw,cap,cookie,ipaddr,cexpire,info,photo FROM user;
   ;
   db_multi_exec(zSQL1);
   
   /* When the replace flag is set, add triggers that run the first time
   ** that new data is seen.  The triggers run only once and delete all the
@@ -273,33 +298,89 @@
   ** existing data.
   */
   if( replaceFlag ){
     static const char zSQL2[] =
       @ CREATE TRIGGER _xfer_r1 BEFORE INSERT ON _xfer_reportfmt
-      @ WHEN flag_test(1) BEGIN
+      @ WHEN NOT config_is_reset(2) BEGIN
       @   DELETE FROM _xfer_reportfmt;
-      @   SELECT flag_clear(1);
+      @   SELECT config_reset(2);
       @ END;
       @ CREATE TRIGGER _xfer_r2 BEFORE INSERT ON _xfer_user
-      @ WHEN flag_test(2) BEGIN
+      @ WHEN NOT config_is_reset(16) BEGIN
       @   DELETE FROM _xfer_user;
-      @   SELECT flag_clear(2);
+      @   SELECT config_reset(16);
       @ END;
       @ CREATE TEMP TRIGGER _xfer_r3 BEFORE INSERT ON shun
-      @ WHEN flag_test(4) BEGIN
+      @ WHEN NOT config_is_reset(8) BEGIN
       @   DELETE FROM shun;
-      @   SELECT flag_clear(4);
+      @   SELECT config_reset(8);
       @ END;
     ;
-    sqlite3_create_function(g.db, "flag_test", 1, SQLITE_UTF8, 0,
-         flag_test_function, 0, 0);
-    sqlite3_create_function(g.db, "flag_clear", 1, SQLITE_UTF8, 0,
-         flag_clear_function, 0, 0);
-    flag_value = 0xffff;
+    sqlite3_create_function(g.db, "config_is_reset", 1, SQLITE_UTF8, 0,
+         config_is_reset_function, 0, 0);
+    sqlite3_create_function(g.db, "config_reset", 1, SQLITE_UTF8, 0,
+         config_reset_function, 0, 0);
+    configHasBeenReset = 0;
     db_multi_exec(zSQL2);
   }
 }
+
+/*
+** After receiving configuration data, call this routine to transfer
+** the results into the main database.
+*/
+void configure_finalize_receive(void){
+  static const char zSQL[] =
+    @ DELETE FROM user;
+    @ INSERT INTO user SELECT * FROM _xfer_user;
+    @ DELETE FROM reportfmt;
+    @ INSERT INTO reportfmt SELECT * FROM _xfer_reportfmt;
+    @ DROP TABLE _xfer_user;
+    @ DROP TABLE _xfer_reportfmt;
+  ;
+  db_multi_exec(zSQL);
+}
+
+/*
+** Return true if z[] is not a "safe" SQL token.  A safe token is one of:
+**
+**   *   A string literal
+**   *   A blob literal
+**   *   An integer literal  (no floating point)
+**   *   NULL
+*/
+static int safeSql(const char *z){
+  int i;
+  if( z==0 || z[0]==0 ) return 0;
+  if( (z[0]=='x' || z[0]=='X') && z[1]=='\'' ) z++;
+  if( z[0]=='\'' ){
+    for(i=1; z[i]; i++){
+      if( z[i]=='\'' ){
+        i++;
+        if( z[i]=='\'' ){ continue; }
+        return z[i]==0;
+      }
+    }
+    return 0;
+  }else{
+    char c;
+    for(i=0; (c = z[i])!=0; i++){
+      if( !fossil_isalnum(c) ) return 0;
+    }
+  }
+  return 1;
+}
+
+/*
+** Return true if z[] consists of nothing but digits
+*/
+static int safeInt(const char *z){
+  int i;
+  if( z==0 || z[0]==0 ) return 0;
+  for(i=0; fossil_isdigit(z[i]); i++){}
+  return z[i]==0;
+}
 
 /*
 ** Process a single "config" card received from the other side of a
 ** sync session.
 **
@@ -344,108 +425,302 @@
 ** table like _fer_reportfmt or _xfer_user.  Such tables must be created
 ** ahead of time using configure_prepare_to_receive().  Then after multiple
 ** calls to this routine, configure_finalize_receive() to transfer the
 ** information received into the true target table.
 */
-void configure_receive(const char *zName, Blob *pContent, int mask){
-  if( (configure_is_exportable(zName) & mask)==0 ) return;
-  if( strcmp(zName, "logo-image")==0 ){
-    Stmt ins;
-    db_prepare(&ins,
-      "REPLACE INTO config(name, value) VALUES(:name, :value)"
-    );
-    db_bind_text(&ins, ":name", zName);
-    db_bind_blob(&ins, ":value", pContent);
-    db_step(&ins);
-    db_finalize(&ins);
-  }else if( zName[0]=='@' ){
-    /* Notice that we are evaluating arbitrary SQL received from the
-    ** client.  But this can only happen if the client has authenticated
-    ** as an administrator, so presumably we trust the client at this
-    ** point.
-    */
-    db_multi_exec("%s", blob_str(pContent));
+void configure_receive(const char *zName, Blob *pContent, int groupMask){
+  if( zName[0]=='/' ){
+    /* The new format */
+    char *azToken[12];
+    int nToken = 0;
+    int ii, jj;
+    int thisMask;
+    Blob name, value, sql;
+    static const struct receiveType {
+      const char *zName;
+      const char *zPrimKey;
+      int nField;
+      const char *azField[4];
+    } aType[] = {
+      { "/config",    "name",  1, { "value", 0, 0, 0 }              },
+      { "@user",      "login", 4, { "pw", "cap", "info", "photo" }  },
+      { "@shun",      "uuid",  1, { "scom", 0, 0, 0 }               },
+      { "@reportfmt", "title", 3, { "owner", "cols", "sqlcode", 0 } },
+      { "@concealed", "hash",  1, { "content", 0, 0, 0 }            },
+    };
+    for(ii=0; ii<count(aType); ii++){
+      if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
+    }
+    if( ii>=count(aType) ) return;
+    while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
+      char *z = blob_terminate(&name);
+      if( !safeSql(z) ) return;
+      if( nToken>0 ){
+        for(jj=0; jj<aType[ii].nField; jj++){
+          if( fossil_strcmp(aType[ii].azField[jj], z)==0 ) break;
+        }
+        if( jj>=aType[ii].nField ) continue;
+      }else{
+        if( !safeInt(z) ) return;
+      }
+      azToken[nToken++] = z;
+      azToken[nToken++] = z = blob_terminate(&value);
+      if( !safeSql(z) ) return;
+      if( nToken>=count(azToken) ) break;
+    }
+    if( nToken<2 ) return;
+    if( aType[ii].zName[0]=='/' ){
+      thisMask = configure_is_exportable(azToken[1]);
+    }else{
+      thisMask = configure_is_exportable(aType[ii].zName);
+    }
+    if( (thisMask & groupMask)==0 ) return;
+    
+    blob_zero(&sql);
+    if( groupMask & CONFIGSET_OVERWRITE ){
+      if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
+        db_multi_exec("DELETE FROM %s", &aType[ii].zName[1]);
+        configHasBeenReset |= thisMask;
+      }
+      blob_append(&sql, "REPLACE INTO ", -1);
+    }else{
+      blob_append(&sql, "INSERT OR IGNORE INTO ", -1);
+    }
+    blob_appendf(&sql, "%s(%s, mtime", &zName[1], aType[ii].zPrimKey);
+    for(jj=2; jj<nToken; jj+=2){
+       blob_appendf(&sql, ",%s", azToken[jj]);
+    }
+    blob_appendf(&sql,") VALUES(%s,%s", azToken[1], azToken[0]);
+    for(jj=2; jj<nToken; jj+=2){
+       blob_appendf(&sql, ",%s", azToken[jj+1]);
+    }
+    db_multi_exec("%s)", blob_str(&sql));
+    if( db_changes()==0 ){
+      blob_reset(&sql);
+      blob_appendf(&sql, "UPDATE %s SET mtime=%s", &zName[1], azToken[0]);
+      for(jj=2; jj<nToken; jj+=2){
+        blob_appendf(&sql, ", %s=%s", azToken[jj], azToken[jj+1]);
+      }
+      blob_appendf(&sql, " WHERE %s=%s AND mtime<%s",
+                   aType[ii].zPrimKey, azToken[1], azToken[0]);
+      db_multi_exec("%s", blob_str(&sql));
+    }
+    blob_reset(&sql);
   }else{
-    db_multi_exec(
-       "REPLACE INTO config(name,value) VALUES(%Q,%Q)",
-       zName, blob_str(pContent)
-    );
+    /* Otherwise, the old format */
+    if( (configure_is_exportable(zName) & groupMask)==0 ) return;
+    if( strcmp(zName, "logo-image")==0 ){
+      Stmt ins;
+      db_prepare(&ins,
+        "REPLACE INTO config(name, value, mtime) VALUES(:name, :value, now())"
+      );
+      db_bind_text(&ins, ":name", zName);
+      db_bind_blob(&ins, ":value", pContent);
+      db_step(&ins);
+      db_finalize(&ins);
+    }else if( zName[0]=='@' ){
+      /* Notice that we are evaluating arbitrary SQL received from the
+      ** client.  But this can only happen if the client has authenticated
+      ** as an administrator, so presumably we trust the client at this
+      ** point.
+      */
+      db_multi_exec("%s", blob_str(pContent));
+    }else{
+      db_multi_exec(
+         "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
+         zName, blob_str(pContent)
+      );
+    }
+  }
+}
+
+/*
+** Process a file full of "config" cards.
+*/
+void configure_receive_all(Blob *pIn, int groupMask){
+  Blob line;
+  int nToken;
+  int size;
+  Blob aToken[4];
+
+  configHasBeenReset = 0;
+  while( blob_line(pIn, &line) ){
+    if( blob_buffer(&line)[0]=='#' ) continue;
+    nToken = blob_tokenize(&line, aToken, count(aToken));
+    if( blob_eq(&aToken[0],"config")
+     && nToken==3
+     && blob_is_int(&aToken[2], &size)
+    ){
+      const char *zName = blob_str(&aToken[1]);
+      Blob content;
+      blob_zero(&content);
+      blob_extract(pIn, size, &content);
+      g.okAdmin = g.okRdAddr = 1;
+      configure_receive(zName, &content, groupMask);
+      blob_reset(&content);
+      blob_seek(pIn, 1, BLOB_SEEK_CUR);
+    }
   }
 }
-
+    
 
 /*
-** After receiving configuration data, call this routine to transfer
-** the results into the main database.
+** Send "config" cards using the new format for all elements of a group
+** that have recently changed.
+**
+** Output goes into pOut.  The groupMask identifies the group(s) to be sent.
+** Send only entries whose timestamp is later than or equal to iStart.
+**
+** Return the number of cards sent.
 */
-void configure_finalize_receive(void){
-  static const char zSQL[] =
-    @ DELETE FROM user;
-    @ INSERT INTO user SELECT * FROM _xfer_user;
-    @ DELETE FROM reportfmt;
-    @ INSERT INTO reportfmt SELECT * FROM _xfer_reportfmt;
-    @ DROP TABLE _xfer_user;
-    @ DROP TABLE _xfer_reportfmt;
-  ;
-  db_multi_exec(zSQL);
+int configure_send_group(
+  Blob *pOut,              /* Write output here */
+  int groupMask,           /* Mask of groups to be send */
+  sqlite3_int64 iStart     /* Only write values changed since this time */
+){
+  Stmt q;
+  Blob rec;
+  int ii;
+  int nCard = 0;
+
+  blob_zero(&rec);
+  if( groupMask & CONFIGSET_SHUN ){
+    db_prepare(&q, "SELECT mtime, quote(uuid), quote(scom) FROM shun"
+                   " WHERE mtime>=%lld", iStart);
+    while( db_step(&q)==SQLITE_ROW ){
+      blob_appendf(&rec,"%s %s scom %s",
+        db_column_text(&q, 0),
+        db_column_text(&q, 1),
+        db_column_text(&q, 2)
+      );
+      blob_appendf(pOut, "config /shun %d\n%s\n",
+                   blob_size(&rec), blob_str(&rec));
+      nCard++;
+      blob_reset(&rec);
+    }
+    db_finalize(&q);
+  }
+  if( groupMask & CONFIGSET_USER ){
+    db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap),"
+                   "       quote(info), quote(photo) FROM user"
+                   " WHERE mtime>=%lld", iStart);
+    while( db_step(&q)==SQLITE_ROW ){
+      blob_appendf(&rec,"%s %s pw %s cap %s info %s photo %s",
+        db_column_text(&q, 0),
+        db_column_text(&q, 1),
+        db_column_text(&q, 2),
+        db_column_text(&q, 3),
+        db_column_text(&q, 4),
+        db_column_text(&q, 5)
+      );
+      blob_appendf(pOut, "config /user %d\n%s\n",
+                   blob_size(&rec), blob_str(&rec));
+      nCard++;
+      blob_reset(&rec);
+    }
+    db_finalize(&q);
+  }
+  if( groupMask & CONFIGSET_TKT ){
+    db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols),"
+                   "       quote(sqlcode) FROM reportfmt"
+                   " WHERE mtime>=%lld", iStart);
+    while( db_step(&q)==SQLITE_ROW ){
+      blob_appendf(&rec,"%s %s owner %s cols %s sqlcode %s",
+        db_column_text(&q, 0),
+        db_column_text(&q, 1),
+        db_column_text(&q, 2),
+        db_column_text(&q, 3),
+        db_column_text(&q, 4)
+      );
+      blob_appendf(pOut, "config /reportfmt %d\n%s\n",
+                   blob_size(&rec), blob_str(&rec));
+      nCard++;
+      blob_reset(&rec);
+    }
+    db_finalize(&q);
+  }
+  if( groupMask & CONFIGSET_ADDR ){
+    db_prepare(&q, "SELECT mtime, quote(hash), quote(content) FROM concealed"
+                   " WHERE mtime>=%lld", iStart);
+    while( db_step(&q)==SQLITE_ROW ){
+      blob_appendf(&rec,"%s %s content %s",
+        db_column_text(&q, 0),
+        db_column_text(&q, 1),
+        db_column_text(&q, 2)
+      );
+      blob_appendf(pOut, "config /concealed %d\n%s\n",
+                   blob_size(&rec), blob_str(&rec));
+      nCard++;
+      blob_reset(&rec);
+    }
+    db_finalize(&q);
+  }
+  db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
+                 " WHERE name=:name AND mtime>=%lld", iStart);
+  for(ii=0; ii<count(aConfig); ii++){
+    if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
+      db_bind_text(&q, ":name", aConfig[ii].zName);
+      while( db_step(&q)==SQLITE_ROW ){
+        blob_appendf(&rec,"%s %s value %s",
+          db_column_text(&q, 0),
+          db_column_text(&q, 1),
+          db_column_text(&q, 2)
+        );
+        blob_appendf(pOut, "config /config %d\n%s\n",
+                     blob_size(&rec), blob_str(&rec));
+        nCard++;
+        blob_reset(&rec);
+      }
+      db_reset(&q);
+    }
+  }
+  db_finalize(&q);
+  return nCard;
 }
 
 /*
 ** Identify a configuration group by name.  Return its mask.
 ** Throw an error if no match.
 */
-static int find_area(const char *z){
+int configure_name_to_mask(const char *z, int notFoundIsFatal){
   int i;
   int n = strlen(z);
   for(i=0; i<count(aGroupName); i++){
-    if( strncmp(z, aGroupName[i].zName, n)==0 ){
+    if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){
       return aGroupName[i].groupMask;
     }
   }
-  printf("Available configuration areas:\n");
-  for(i=0; i<count(aGroupName); i++){
-    printf("  %-10s %s\n", aGroupName[i].zName, aGroupName[i].zHelp);
+  if( notFoundIsFatal ){
+    printf("Available configuration areas:\n");
+    for(i=0; i<count(aGroupName); i++){
+      printf("  %-10s %s\n", &aGroupName[i].zName[1], aGroupName[i].zHelp);
+    }
+    fossil_fatal("no such configuration area: \"%s\"", z);
   }
-  fossil_fatal("no such configuration area: \"%s\"", z);
   return 0;
 }
 
 /*
 ** Write SQL text into file zFilename that will restore the configuration
 ** area identified by mask to its current state from any other state.
 */
 static void export_config(
-  int mask,                 /* Mask indicating which configuration to export */
+  int groupMask,            /* Mask indicating which configuration to export */
   const char *zMask,        /* Name of the configuration */
+  sqlite3_int64 iStart,     /* Start date */
   const char *zFilename     /* Write into this file */
 ){
-  int i;
   Blob out;
   blob_zero(&out);
   blob_appendf(&out, 
-    "-- The \"%s\" configuration exported from\n"
-    "-- repository \"%s\"\n"
-    "-- on %s\n",
+    "# The \"%s\" configuration exported from\n"
+    "# repository \"%s\"\n"
+    "# on %s\n",
     zMask, g.zRepositoryName,
     db_text(0, "SELECT datetime('now')")
   );
-  for(i=0; i<count(aConfig); i++){
-    if( (aConfig[i].groupMask & mask)!=0 ){
-      const char *zName = aConfig[i].zName;
-      if( zName[0]!='@' ){
-        char *zValue = db_text(0, 
-            "SELECT quote(value) FROM config WHERE name=%Q", zName);
-        if( zValue ){
-          blob_appendf(&out,"REPLACE INTO config VALUES(%Q,%s);\n", 
-                       zName, zValue);
-        }
-        free(zValue);
-      }else{
-        configure_render_special_name(zName, &out);
-      }
-    }
-  }
+  configure_send_group(&out, groupMask, iStart);
   blob_write_to_file(&out, zFilename);
   blob_reset(&out);
 }
 
 
@@ -475,25 +750,31 @@
 **
 **    %fossil configuration pull AREA ?URL?
 **
 **         Pull and install the configuration from a different server
 **         identified by URL.  If no URL is specified, then the default
-**         server is used. 
+**         server is used. Use the --legacy option for the older protocol
+**         (when talking to servers compiled prior to 2011-04-27.)  Use
+**         the --overwrite flag to completely replace local settings with
+**         content received from URL.
 **
 **    %fossil configuration push AREA ?URL?
 **
 **         Push the local configuration into the remote server identified
 **         by URL.  Admin privilege is required on the remote server for
-**         this to work.
+**         this to work.  When the same record exists both locally and on
+**         the remote end, the one that was most recently changed wins.
+**         Use the --legacy flag when talking to holder servers.
 **
 **    %fossil configuration reset AREA
 **
 **         Restore the configuration to the default.  AREA as above.
 **
-** WARNING: Do not import, merge, or pull configurations from an untrusted
-** source.  The inbound configuration is not checked for safety and can
-** introduce security vulnerabilities.
+**    %fossil configuration sync AREA ?URL?
+**
+**         Synchronize configuration changes in the local repository with
+**         the remote repository at URL.  
 */
 void configuration_cmd(void){
   int n;
   const char *zMethod;
   if( g.argc<3 ){
@@ -502,36 +783,59 @@
   db_find_and_open_repository(0, 0);
   zMethod = g.argv[2];
   n = strlen(zMethod);
   if( strncmp(zMethod, "export", n)==0 ){
     int mask;
+    const char *zSince = find_option("since",0,1);
+    sqlite3_int64 iStart;
     if( g.argc!=5 ){
       usage("export AREA FILENAME");
     }
-    mask = find_area(g.argv[3]);
-    export_config(mask, g.argv[3], g.argv[4]);
+    mask = configure_name_to_mask(g.argv[3], 1);
+    if( zSince ){
+      iStart = db_multi_exec(
+         "SELECT coalesce(strftime('%%s',%Q),strftime('%%s','now',%Q))+0",
+         zSince, zSince
+      );
+    }else{
+      iStart = 0;
+    }
+    export_config(mask, g.argv[3], iStart, g.argv[4]);
   }else
   if( strncmp(zMethod, "import", n)==0 
        || strncmp(zMethod, "merge", n)==0 ){
     Blob in;
+    int groupMask;
     if( g.argc!=4 ) usage(mprintf("%s FILENAME",zMethod));
     blob_read_from_file(&in, g.argv[3]);
     db_begin_transaction();
-    configure_prepare_to_receive(zMethod[0]=='i');
-    db_multi_exec("%s", blob_str(&in));
-    configure_finalize_receive();
+    if( zMethod[0]=='i' ){
+      groupMask = CONFIGSET_ALL | CONFIGSET_OVERWRITE;
+    }else{
+      groupMask = CONFIGSET_ALL;
+    }
+    configure_receive_all(&in, groupMask);
     db_end_transaction(0);
   }else
-  if( strncmp(zMethod, "pull", n)==0 || strncmp(zMethod, "push", n)==0 ){
+  if( strncmp(zMethod, "pull", n)==0
+   || strncmp(zMethod, "push", n)==0
+   || strncmp(zMethod, "sync", n)==0
+  ){
     int mask;
     const char *zServer;
     const char *zPw;
+    int legacyFlag = 0;
+    int overwriteFlag = 0;
+    if( zMethod[0]!='s' ) legacyFlag = find_option("legacy",0,0)!=0;
+    if( strncmp(zMethod,"pull",n)==0 ){
+      overwriteFlag = find_option("overwrite",0,0)!=0;
+    }
     url_proxy_options();
     if( g.argc!=4 && g.argc!=5 ){
       usage("pull AREA ?URL?");
     }
-    mask = find_area(g.argv[3]);
+    mask = configure_name_to_mask(g.argv[3], 1);
     if( g.argc==5 ){
       zServer = g.argv[4];
       zPw = 0;
       g.dontKeepUrl = 1;
     }else{
@@ -543,25 +847,29 @@
     }
     url_parse(zServer);
     if( g.urlPasswd==0 && zPw ) g.urlPasswd = mprintf("%s", zPw);
     user_select();
     url_enable_proxy("via proxy: ");
+    if( legacyFlag ) mask |= CONFIGSET_OLDFORMAT;
+    if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE;
     if( strncmp(zMethod, "push", n)==0 ){
       client_sync(0,0,0,0,0,mask);
-    }else{
+    }else if( strncmp(zMethod, "pull", n)==0 ){
       client_sync(0,0,0,0,mask,0);
+    }else{
+      client_sync(0,0,0,0,mask,mask);
     }
   }else
   if( strncmp(zMethod, "reset", n)==0 ){
     int mask, i;
     char *zBackup;
     if( g.argc!=4 ) usage("reset AREA");
-    mask = find_area(g.argv[3]);
+    mask = configure_name_to_mask(g.argv[3], 1);
     zBackup = db_text(0, 
        "SELECT strftime('config-backup-%%Y%%m%%d%%H%%M%%f','now')");
     db_begin_transaction();
-    export_config(mask, g.argv[3], zBackup);
+    export_config(mask, g.argv[3], 0, zBackup);
     for(i=0; i<count(aConfig); i++){
       const char *zName = aConfig[i].zName;
       if( (aConfig[i].groupMask & mask)==0 ) continue;
       if( zName[0]!='@' ){
         db_multi_exec("DELETE FROM config WHERE name=%Q", zName);

Index: src/db.c
==================================================================
--- src/db.c
+++ src/db.c
@@ -34,10 +34,11 @@
 #endif
 #include <sqlite3.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
+#include <time.h>
 #include "db.h"
 
 #if INTERFACE
 /*
 ** An single SQL statement is represented as an instance of the following
@@ -606,10 +607,23 @@
   }
   va_end(ap);
   sqlite3_exec(db, "COMMIT", 0, 0, 0);
   sqlite3_close(db);
 }
+
+/*
+** Function to return the number of seconds since 1970.  This is
+** the same as strftime('%s','now') but is more compact.
+*/
+static void db_now_function(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  sqlite3_result_int64(context, time(0));
+}
+
 
 /*
 ** Open a database file.  Return a pointer to the new database
 ** connection.  An error results in process abort.
 */
@@ -630,10 +644,11 @@
   if( rc!=SQLITE_OK ){
     db_err(sqlite3_errmsg(db));
   }
   sqlite3_busy_timeout(db, 5000); 
   sqlite3_wal_autocheckpoint(db, 1);  /* Set to checkpoint frequently */
+  sqlite3_create_function(db, "now", 0, SQLITE_ANY, 0, db_now_function, 0, 0);
   return db;
 }
 
 
 /*
@@ -1105,14 +1120,14 @@
 
   db_set("content-schema", CONTENT_SCHEMA, 0);
   db_set("aux-schema", AUX_SCHEMA, 0);
   if( makeServerCodes ){
     db_multi_exec(
-      "INSERT INTO config(name,value)"
-      " VALUES('server-code', lower(hex(randomblob(20))));"
-      "INSERT INTO config(name,value)"
-      " VALUES('project-code', lower(hex(randomblob(20))));"
+      "INSERT INTO config(name,value,mtime)"
+      " VALUES('server-code', lower(hex(randomblob(20))),now());"
+      "INSERT INTO config(name,value,mtime)"
+      " VALUES('project-code', lower(hex(randomblob(20))),now());"
     );
   }
   if( !db_is_global("autosync") ) db_set_int("autosync", 1, 0);
   if( !db_is_global("localauth") ) db_set_int("localauth", 0, 0);
   db_create_default_users(0, zDefaultUser);
@@ -1295,11 +1310,12 @@
     sha1sum_step_text(zContent, n);
     sha1sum_finish(&out);
     sqlite3_snprintf(sizeof(zHash), zHash, "%s", blob_str(&out));
     blob_reset(&out);
     db_multi_exec(
-       "INSERT OR IGNORE INTO concealed VALUES(%Q,%#Q)",
+       "INSERT OR IGNORE INTO concealed(hash,content,mtime)"
+       " VALUES(%Q,%#Q,now())",
        zHash, n, zContent
     );
   }
   return zHash;
 }
@@ -1406,11 +1422,11 @@
     db_swap_connections();
     db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)",
                    zName, zValue);
     db_swap_connections();
   }else{
-    db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%Q)",
+    db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
                    zName, zValue);
   }
   if( globalFlag && g.repositoryOpen ){
     db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
   }
@@ -1465,11 +1481,11 @@
     db_swap_connections();
     db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)",
                   zName, value);
     db_swap_connections();
   }else{
-    db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%d)",
+    db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())",
                   zName, value);
   }
   if( globalFlag && g.repositoryOpen ){
     db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
   }

Index: src/login.c
==================================================================
--- src/login.c
+++ src/login.c
@@ -1257,12 +1257,12 @@
   db_multi_exec("DETACH other");
 
   /* Propagate the changes to all other members of the login-group */
   zSql = mprintf(
     "BEGIN;"
-    "REPLACE INTO config(name, value) VALUES('peer-name-%q', %Q);"
-    "REPLACE INTO config(name, value) VALUES('peer-repo-%q', %Q);"
+    "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());"
+    "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());"
     "COMMIT;",
     zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
   );
   login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
   fossil_free(zSql);

Index: src/rebuild.c
==================================================================
--- src/rebuild.c
+++ src/rebuild.c
@@ -22,13 +22,15 @@
 #include <assert.h>
 #include <dirent.h>
 #include <errno.h>
 
 /*
-** Schema changes
+** Make changes to the stable part of the schema (the part that is not
+** simply deleted and reconstructed on a rebuild) to bring the schema
+** up to the latest.
 */
-static const char zSchemaUpdates[] =
+static const char zSchemaUpdates1[] =
 @ -- Index on the delta table
 @ --
 @ CREATE INDEX IF NOT EXISTS delta_i1 ON delta(srcid);
 @
 @ -- Artifacts that should not be processed are identified in the
@@ -39,41 +41,124 @@
 @ --
 @ -- Shunned artifacts do not exist in the blob table.  Hence they
 @ -- have not artifact ID (rid) and we thus must store their full
 @ -- UUID.
 @ --
-@ CREATE TABLE IF NOT EXISTS shun(uuid UNIQUE);
+@ CREATE TABLE IF NOT EXISTS shun(
+@   uuid UNIQUE,          -- UUID of artifact to be shunned. Canonical form
+@   mtime INTEGER,        -- When added.  Seconds since 1970
+@   scom TEXT             -- Optional text explaining why the shun occurred
+@ );
 @
 @ -- Artifacts that should not be pushed are stored in the "private"
 @ -- table.  
 @ --
 @ CREATE TABLE IF NOT EXISTS private(rid INTEGER PRIMARY KEY);
 @
-@ -- An entry in this table describes a database query that generates a
-@ -- table of tickets.
-@ --
-@ CREATE TABLE IF NOT EXISTS reportfmt(
-@    rn integer primary key,  -- Report number
-@    owner text,              -- Owner of this report format (not used)
-@    title text,              -- Title of this report
-@    cols text,               -- A color-key specification
-@    sqlcode text             -- An SQL SELECT statement for this report
-@ );
-@
 @ -- Some ticket content (such as the originators email address or contact
 @ -- information) needs to be obscured to protect privacy.  This is achieved
 @ -- by storing an SHA1 hash of the content.  For display, the hash is
 @ -- mapped back into the original text using this table.  
 @ --
 @ -- This table contains sensitive information and should not be shared
 @ -- with unauthorized users.
 @ --
 @ CREATE TABLE IF NOT EXISTS concealed(
-@   hash TEXT PRIMARY KEY,
-@   content TEXT
+@   hash TEXT PRIMARY KEY,    -- The SHA1 hash of content
+@   mtime INTEGER,            -- Time created.  Seconds since 1970
+@   content TEXT              -- Content intended to be concealed
+@ );
+;
+static const char zSchemaUpdates2[] =
+@ -- An entry in this table describes a database query that generates a
+@ -- table of tickets.
+@ --
+@ CREATE TABLE IF NOT EXISTS reportfmt(
+@    rn INTEGER PRIMARY KEY,  -- Report number
+@    owner TEXT,              -- Owner of this report format (not used)
+@    title TEXT UNIQUE,       -- Title of this report
+@    mtime INTEGER,           -- Time last modified.  Seconds since 1970
+@    cols TEXT,               -- A color-key specification
+@    sqlcode TEXT             -- An SQL SELECT statement for this report
 @ );
 ;
+
+static void rebuild_update_schema(void){
+  int rc;
+  db_multi_exec(zSchemaUpdates1);
+  db_multi_exec(zSchemaUpdates2);
+
+  rc = db_exists("SELECT 1 FROM sqlite_master"
+                 " WHERE name='user' AND sql GLOB '* mtime *'");
+  if( rc==0 ){
+    db_multi_exec(
+      "CREATE TEMP TABLE temp_user AS SELECT * FROM user;"
+      "DROP TABLE user;"
+      "CREATE TABLE user(\n"
+      "  uid INTEGER PRIMARY KEY,\n"
+      "  login TEXT UNIQUE,\n"
+      "  pw TEXT,\n"
+      "  cap TEXT,\n"
+      "  cookie TEXT,\n"
+      "  ipaddr TEXT,\n"
+      "  cexpire DATETIME,\n"
+      "  info TEXT,\n"
+      "  mtime DATE,\n"
+      "  photo BLOB\n"
+      ");"
+      "INSERT OR IGNORE INTO user"
+        " SELECT uid, login, pw, cap, cookie,"
+               " ipaddr, cexpire, info, now(), photo FROM temp_user;"
+      "DROP TABLE temp_user;"
+    );
+  }
+
+  rc = db_exists("SELECT 1 FROM sqlite_master"
+                 " WHERE name='config' AND sql GLOB '* mtime *'");
+  if( rc==0 ){
+    db_multi_exec(
+      "ALTER TABLE config ADD COLUMN mtime INTEGER;"
+      "UPDATE config SET mtime=now();"
+    );
+  }
+
+  rc = db_exists("SELECT 1 FROM sqlite_master"
+                 " WHERE name='shun' AND sql GLOB '* mtime *'");
+  if( rc==0 ){
+    db_multi_exec(
+      "ALTER TABLE shun ADD COLUMN mtime INTEGER;"
+      "ALTER TABLE shun ADD COLUMN scom TEXT;"
+      "UPDATE shun SET mtime=now();"
+    );
+  }
+
+  rc = db_exists("SELECT 1 FROM sqlite_master"
+                 " WHERE name='reportfmt' AND sql GLOB '* mtime *'");
+  if( rc==0 ){
+    db_multi_exec(
+      "CREATE TEMP TABLE old_fmt AS SELECT * FROM reportfmt;"
+      "DROP TABLE reportfmt;"
+    );
+    db_multi_exec(zSchemaUpdates2);
+    db_multi_exec(
+      "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)"
+        " SELECT rn, owner, title, cols, sqlcode, now() FROM old_fmt;"
+      "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)"
+        " SELECT rn, owner, title || ' (' || rn || ')', cols, sqlcode, now()"
+        "   FROM old_fmt;"
+    );
+  }
+
+  rc = db_exists("SELECT 1 FROM sqlite_master"
+                 " WHERE name='concealed' AND sql GLOB '* mtime *'");
+  if( rc==0 ){
+    db_multi_exec(
+      "ALTER TABLE concealed ADD COLUMN mtime INTEGER;"
+      "UPDATE concealed SET mtime=now();"
+    );
+  }
+}  
 
 /*
 ** Variables used to store state information about an on-going "rebuild"
 ** or "deconstruct".
 */
@@ -256,11 +341,11 @@
   ttyOutput = doOut;
   processCnt = 0;
   if (!g.fQuiet) {
     percent_complete(0);
   }
-  db_multi_exec(zSchemaUpdates);
+  rebuild_update_schema();
   for(;;){
     zTable = db_text(0,
        "SELECT name FROM sqlite_master /*scan*/"
        " WHERE type='table'"
        " AND name NOT IN ('blob','delta','rcvfrom','user',"
@@ -459,12 +544,12 @@
   }
   db_begin_transaction();
   ttyOutput = 1;
   errCnt = rebuild_db(randomizeFlag, 1, doClustering);
   db_multi_exec(
-    "REPLACE INTO config(name,value) VALUES('content-schema','%s');"
-    "REPLACE INTO config(name,value) VALUES('aux-schema','%s');",
+    "REPLACE INTO config(name,value,mtime) VALUES('content-schema','%s',now());"
+    "REPLACE INTO config(name,value,mtime) VALUES('aux-schema','%s',now());",
     CONTENT_SCHEMA, AUX_SCHEMA
   );
   if( errCnt && !forceFlag ){
     printf("%d errors. Rolling back changes. Use --force to force a commit.\n",
             errCnt);

Index: src/report.c
==================================================================
--- src/report.c
+++ src/report.c
@@ -360,19 +360,25 @@
     }else if( (zTitle = trim_string(zTitle))[0]==0 ){
       zErr = "Please supply a title"; 
     }else{
       zErr = verify_sql_statement(zSQL);
     }
+    if( zErr==0
+     && db_exists("SELECT 1 FROM reportfmt WHERE title=%Q and rn<>%d",
+                  zTitle, rn)
+    ){
+      zErr = mprintf("There is already another report named \"%h\"", zTitle);
+    }
     if( zErr==0 ){
       login_verify_csrf_secret();
       if( rn>0 ){
         db_multi_exec("UPDATE reportfmt SET title=%Q, sqlcode=%Q,"
-                      " owner=%Q, cols=%Q WHERE rn=%d",
+                      " owner=%Q, cols=%Q, mtime=now() WHERE rn=%d",
            zTitle, zSQL, zOwner, zClrKey, rn);
       }else{
-        db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols) "
-           "VALUES(%Q,%Q,%Q,%Q)",
+        db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols,mtime) "
+           "VALUES(%Q,%Q,%Q,%Q,now())",
            zTitle, zSQL, zOwner, zClrKey);
         rn = db_last_insert_rowid();
       }
       cgi_redirect(mprintf("rptview?rn=%d", rn));
       return;

Index: src/schema.c
==================================================================
--- src/schema.c
+++ src/schema.c
@@ -39,12 +39,12 @@
 ** changes.  The aux tables have an arbitrary version number (typically
 ** a date) which can change frequently.  When the content schema changes,
 ** we have to execute special procedures to update the schema.  When
 ** the aux schema changes, all we need to do is rebuild the database.
 */
-#define CONTENT_SCHEMA  "1"
-#define AUX_SCHEMA      "2011-02-25 14:52"
+#define CONTENT_SCHEMA  "2"
+#define AUX_SCHEMA      "2011-04-25 19:50"
 
 #endif /* INTERFACE */
 
 
 /*
@@ -51,21 +51,25 @@
 ** The schema for a repository database.  
 **
 ** Schema1[] contains parts of the schema that are fixed and unchanging
 ** across versions.  Schema2[] contains parts of the schema that can
 ** change from one version to the next.  The information in Schema2[]
-** can be reconstructed from the information in Schema1[].
+** is reconstructed from the information in Schema1[] by the "rebuild"
+** operation.
 */
 const char zRepositorySchema1[] = 
 @ -- The BLOB and DELTA tables contain all records held in the repository.
 @ --
-@ -- The BLOB.CONTENT column is always compressed using libz.  This
+@ -- The BLOB.CONTENT column is always compressed using zlib.  This
 @ -- column might hold the full text of the record or it might hold
 @ -- a delta that is able to reconstruct the record from some other
 @ -- record.  If BLOB.CONTENT holds a delta, then a DELTA table entry
 @ -- will exist for the record and that entry will point to another
 @ -- entry that holds the source of the delta.  Deltas can be chained.
+@ --
+@ -- The blob and delta tables collectively hold the "global state" of
+@ -- a Fossil repository.  
 @ --
 @ CREATE TABLE blob(
 @   rid INTEGER PRIMARY KEY,        -- Record ID
 @   rcvid INTEGER,                  -- Origin of this record
 @   size INTEGER,                   -- Size of content. -1 for a phantom.
@@ -77,17 +81,24 @@
 @   rid INTEGER PRIMARY KEY,                 -- Record ID
 @   srcid INTEGER NOT NULL REFERENCES blob   -- Record holding source document
 @ );
 @ CREATE INDEX delta_i1 ON delta(srcid);
 @
+@ -------------------------------------------------------------------------
+@ -- The BLOB and DELTA tables above hold the "global state" of a Fossil
+@ -- project; the stuff that is normally exchanged during "sync".  The
+@ -- "local state" of a repository is contained in the remaining tables of
+@ -- the zRepositorySchema1 string.  
+@ -------------------------------------------------------------------------
+@
 @ -- Whenever new blobs are received into the repository, an entry
 @ -- in this table records the source of the blob.
 @ --
 @ CREATE TABLE rcvfrom(
 @   rcvid INTEGER PRIMARY KEY,      -- Received-From ID
 @   uid INTEGER REFERENCES user,    -- User login
-@   mtime DATETIME,                 -- Time or receipt
+@   mtime DATETIME,                 -- Time of receipt.  Julian day.
 @   nonce TEXT UNIQUE,              -- Nonce used for login
 @   ipaddr TEXT                     -- Remote IP address.  NULL for direct.
 @ );
 @
 @ -- Information about users
@@ -99,26 +110,28 @@
 @ -- hash based on the project-code, the user login, and the cleartext
 @ -- password.
 @ --
 @ CREATE TABLE user(
 @   uid INTEGER PRIMARY KEY,        -- User ID
-@   login TEXT,                     -- login name of the user
+@   login TEXT UNIQUE,              -- login name of the user
 @   pw TEXT,                        -- password
 @   cap TEXT,                       -- Capabilities of this user
 @   cookie TEXT,                    -- WWW login cookie
 @   ipaddr TEXT,                    -- IP address for which cookie is valid
 @   cexpire DATETIME,               -- Time when cookie expires
 @   info TEXT,                      -- contact information
+@   mtime DATE,                     -- last change.  seconds since 1970
 @   photo BLOB                      -- JPEG image of this user
 @ );
 @
 @ -- The VAR table holds miscellanous information about the repository.
 @ -- in the form of name-value pairs.
 @ --
 @ CREATE TABLE config(
 @   name TEXT PRIMARY KEY NOT NULL,  -- Primary name of the entry
 @   value CLOB,                      -- Content of the named parameter
+@   mtime DATE,                      -- last modified.  seconds since 1970
 @   CHECK( typeof(name)='text' AND length(name)>=1 )
 @ );
 @
 @ -- Artifacts that should not be processed are identified in the
 @ -- "shun" table.  Artifacts that are control-file forgeries or
@@ -128,11 +141,15 @@
 @ --
 @ -- Shunned artifacts do not exist in the blob table.  Hence they
 @ -- have not artifact ID (rid) and we thus must store their full
 @ -- UUID.
 @ --
-@ CREATE TABLE shun(uuid UNIQUE);
+@ CREATE TABLE shun(
+@   uuid UNIQUE,          -- UUID of artifact to be shunned. Canonical form
+@   mtime DATE,           -- When added.  seconds since 1970
+@   scom TEXT             -- Optional text explaining why the shun occurred
+@ );
 @
 @ -- Artifacts that should not be pushed are stored in the "private"
 @ -- table.  Private artifacts are omitted from the "unclustered" and
 @ -- "unsent" tables.
 @ --
@@ -140,17 +157,19 @@
 @
 @ -- An entry in this table describes a database query that generates a
 @ -- table of tickets.
 @ --
 @ CREATE TABLE reportfmt(
-@    rn integer primary key,  -- Report number
-@    owner text,              -- Owner of this report format (not used)
-@    title text,              -- Title of this report
-@    cols text,               -- A color-key specification
-@    sqlcode text             -- An SQL SELECT statement for this report
+@    rn INTEGER PRIMARY KEY,  -- Report number
+@    owner TEXT,              -- Owner of this report format (not used)
+@    title TEXT UNIQUE,       -- Title of this report
+@    mtime DATE,              -- Last modified.  seconds since 1970
+@    cols TEXT,               -- A color-key specification
+@    sqlcode TEXT             -- An SQL SELECT statement for this report
 @ );
-@ INSERT INTO reportfmt(title,cols,sqlcode) VALUES('All Tickets','#ffffff Key:
+@ INSERT INTO reportfmt(title,mtime,cols,sqlcode) 
+@ VALUES('All Tickets',julianday('1970-01-01'),'#ffffff Key:
 @ #f2dcdc Active
 @ #e8e8e8 Review
 @ #cfe8bd Fixed
 @ #bde5d6 Tested
 @ #cacae5 Deferred
@@ -176,12 +195,13 @@
 @ --
 @ -- This table contains sensitive information and should not be shared
 @ -- with unauthorized users.
 @ --
 @ CREATE TABLE concealed(
-@   hash TEXT PRIMARY KEY,
-@   content TEXT
+@   hash TEXT PRIMARY KEY,    -- The SHA1 hash of content
+@   mtime DATE,               -- Time created.  Seconds since 1970
+@   content TEXT              -- Content intended to be concealed
 @ );
 ;
 
 const char zRepositorySchema2[] =
 @ -- Filenames
@@ -214,11 +234,11 @@
 @ --
 @ CREATE TABLE plink(
 @   pid INTEGER REFERENCES blob,    -- Parent manifest
 @   cid INTEGER REFERENCES blob,    -- Child manifest
 @   isprim BOOLEAN,                 -- pid is the primary parent of cid
-@   mtime DATETIME,                 -- the date/time stamp on cid
+@   mtime DATETIME,                 -- the date/time stamp on cid.  Julian day.
 @   UNIQUE(pid, cid)
 @ );
 @ CREATE INDEX plink_i2 ON plink(cid,pid);
 @
 @ -- A "leaf" checkin is a checkin that has no children in the same
@@ -232,11 +252,11 @@
 @
 @ -- Events used to generate a timeline
 @ --
 @ CREATE TABLE event(
 @   type TEXT,                      -- Type of event: 'ci', 'w', 'e', 't'
-@   mtime DATETIME,                 -- Date and time when the event occurs
+@   mtime DATETIME,                 -- Time of occurrence. Julian day.
 @   objid INTEGER PRIMARY KEY,      -- Associated record ID
 @   tagid INTEGER,                  -- Associated ticket or wiki name tag
 @   uid INTEGER REFERENCES user,    -- User who caused the event
 @   bgcolor TEXT,                   -- Color set by 'bgcolor' property
 @   euser TEXT,                     -- User set by 'user' property
@@ -319,11 +339,11 @@
 @   tagid INTEGER REFERENCES tag,   -- The tag that added or removed
 @   tagtype INTEGER,                -- 0:-,cancel  1:+,single  2:*,propagate
 @   srcid INTEGER REFERENCES blob,  -- Artifact of tag. 0 for propagated tags
 @   origid INTEGER REFERENCES blob, -- check-in holding propagated tag
 @   value TEXT,                     -- Value of the tag.  Might be NULL.
-@   mtime TIMESTAMP,                -- Time of addition or removal
+@   mtime TIMESTAMP,                -- Time of addition or removal. Julian day
 @   rid INTEGER REFERENCE blob,     -- Artifact tag is applied to
 @   UNIQUE(rid, tagid)
 @ );
 @ CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime);
 @
@@ -334,11 +354,11 @@
 @ --
 @ 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
+@   mtime TIMESTAMP,       -- time that the hyperlink was added. Julian day.
 @   UNIQUE(target, srctype, srcid)
 @ );
 @ CREATE INDEX backlink_src ON backlink(srcid, srctype);
 @
 @ -- Each attachment is an entry in the following table.  Only
@@ -345,11 +365,11 @@
 @ -- 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
+@   mtime TIMESTAMP,                -- Last changed.  Julian day.
 @   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
@@ -443,11 +463,11 @@
 @   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
+@   mtime INTEGER,                    -- Mtime of file on disk. sec since 1970
 @   pathname TEXT,                    -- Full pathname relative to root
 @   origname TEXT,                    -- Original pathname. NULL if unchanged
 @   UNIQUE(pathname,vid)
 @ );
 @

Index: src/setup.c
==================================================================
--- src/setup.c
+++ src/setup.c
@@ -354,12 +354,12 @@
       style_footer();
       return;
     }
     login_verify_csrf_secret();
     db_multi_exec(
-       "REPLACE INTO user(uid,login,info,pw,cap) "
-       "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')",
+       "REPLACE INTO user(uid,login,info,pw,cap,mtime) "
+       "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s',now())",
       uid, P("login"), P("info"), zPw, zCap
     );
     if( atoi(PD("all","0"))>0 ){
       Blob sql;
       char *zErr = 0;
@@ -375,11 +375,12 @@
       blob_appendf(&sql, 
         "UPDATE user SET login=%Q,"
         "  pw=coalesce(shared_secret(%Q,%Q,"
                 "(SELECT value FROM config WHERE name='project-code')),pw),"
         "  info=%Q,"
-        "  cap=%Q"
+        "  cap=%Q,"
+        "  mtime=now()"
         " WHERE login=%Q;",
         zLogin, P("pw"), zLogin, P("info"), zCap,
         zOldLogin
       );
       login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
@@ -1286,18 +1287,18 @@
   if( P("set")!=0 && zMime && zMime[0] && szImg>0 ){
     Blob img;
     Stmt ins;
     blob_init(&img, aImg, szImg);
     db_prepare(&ins,
-        "REPLACE INTO config(name, value)"
-        " VALUES('logo-image',:bytes)"
+        "REPLACE INTO config(name,value,mtime)"
+        " VALUES('logo-image',:bytes,now())"
     );
     db_bind_blob(&ins, ":bytes", &img);
     db_step(&ins);
     db_finalize(&ins);
     db_multi_exec(
-       "REPLACE INTO config(name, value) VALUES('logo-mimetype',%Q)",
+       "REPLACE INTO config(name,value,mtime) VALUES('logo-mimetype',%Q,now())",
        zMime
     );
     db_end_transaction(0);
     cgi_redirect("setup_logo");
   }else if( P("clr")!=0 ){

Index: src/shun.c
==================================================================
--- src/shun.c
+++ src/shun.c
@@ -81,17 +81,31 @@
       @ <b>fossil rebuild</b> command-line before the artifact content
       @ can pulled in from other respositories.</p>
     }
   }
   if( zUuid && P("add") ){
+    int rid, tagid;
     login_verify_csrf_secret();
-    db_multi_exec("INSERT OR IGNORE INTO shun VALUES('%s')", zUuid);
+    db_multi_exec(
+      "INSERT OR IGNORE INTO shun(uuid,mtime)"
+      " VALUES('%s', now())", zUuid);
     @ <p class="shunned">Artifact
     @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> has been
     @ shunned.  It will no longer be pushed.
     @ It will be removed from the repository the next time the respository
     @ is rebuilt using the <b>fossil rebuild</b> command-line</p>
+    db_multi_exec("DELETE FROM attachment WHERE src=%Q", zUuid);
+    rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid);
+    if( rid ){
+      db_multi_exec("DELETE FROM event WHERE objid=%d", rid);
+    }
+    tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", zUuid);
+    if( tagid ){
+      db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", zUuid);
+      db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid);
+      db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid);
+    }
   }
   @ <p>A shunned artifact will not be pushed nor accepted in a pull and the
   @ artifact content will be purged from the repository the next time the
   @ repository is rebuilt.  A list of shunned artifacts can be seen at the
   @ bottom of this page.</p>

Index: src/skins.c
==================================================================
--- src/skins.c
+++ src/skins.c
@@ -25,11 +25,12 @@
 /*
 ** A black-and-white theme with the project title in a bar across the top
 ** and no logo image.
 */
 static const char zBuiltinSkin1[] = 
-@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
+@ REPLACE INTO config(name,mtime,value)
+@ VALUES('css',now(),'/* General settings for the entire page */
 @ body {
 @   margin: 0ex 1ex;
 @   padding: 0px;
 @   background-color: white;
 @   font-family: sans-serif;
@@ -152,11 +153,11 @@
 @ table.label-value th {
 @   vertical-align: top;
 @   text-align: right;
 @   padding: 0.2ex 2ex;
 @ }');
-@ REPLACE INTO config VALUES('header','<html>
+@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$home/timeline.rss">
 @ <link rel="stylesheet" href="$home/style.css?blackwhite" type="text/css"
@@ -204,11 +205,12 @@
 @ } else {
 @   html "<a href=''$home/login''>Login</a> "
 @ }
 @ </th1></div>
 @ ');
-@ REPLACE INTO config VALUES('footer','<div class="footer">
+@ REPLACE INTO config(name,mtime,value)
+@ VALUES('footer',now(),'<div class="footer">
 @ Fossil version $manifest_version $manifest_date 
 @ </div>
 @ </body></html>
 @ ');
 ;
@@ -216,11 +218,12 @@
 /*
 ** A tan theme with the project title above the user identification
 ** and no logo image.
 */
 static const char zBuiltinSkin2[] = 
-@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
+@ REPLACE INTO config(name,mtime,value)
+@ VALUES('css',now(),'/* General settings for the entire page */
 @ body {
 @   margin: 0ex 0ex;
 @   padding: 0px;
 @   background-color: #fef3bc;
 @   font-family: sans-serif;
@@ -354,11 +357,11 @@
 @   vertical-align: top;
 @   text-align: right;
 @   padding: 0.2ex 2ex;
 @ }
 @ ');
-@ REPLACE INTO config VALUES('header','<html>
+@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$home/timeline.rss">
 @ <link rel="stylesheet" href="$home/style.css?tan" type="text/css"
@@ -405,11 +408,12 @@
 @ } else {
 @   html "<a href=''$home/login''>Login</a> "
 @ }
 @ </th1></div>
 @ ');
-@ REPLACE INTO config VALUES('footer','<div class="footer">
+@ REPLACE INTO config(name,mtime,value)
+@ VALUES('footer',now(),'<div class="footer">
 @ Fossil version $manifest_version $manifest_date
 @ </div>
 @ </body></html>
 @ ');
 ;
@@ -417,11 +421,12 @@
 /*
 ** Black letters on a white or cream background with the main menu
 ** stuck on the left-hand side.
 */
 static const char zBuiltinSkin3[] = 
-@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
+@ REPLACE INTO config(name,mtime,value)
+@ VALUES('css',now(),'/* General settings for the entire page */
 @ body {
 @     margin:0px 0px 0px 0px;
 @     padding:0px;
 @     font-family:verdana, arial, helvetica, "sans serif";
 @     color:#333;
@@ -586,11 +591,11 @@
 @ table.label-value th {
 @   vertical-align: top;
 @   text-align: right;
 @   padding: 0.2ex 2ex;
 @ }');
-@ REPLACE INTO config VALUES('header','<html>
+@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$home/timeline.rss">
 @ <link rel="stylesheet" href="$home/style.css?black2" type="text/css"
@@ -640,11 +645,11 @@
 @   html "<li><a href=''$home/login''>Login</a></li>"
 @ }
 @ </th1></ul></div>
 @ <div id="container">
 @ ');
-@ REPLACE INTO config VALUES('footer','</div>
+@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),'</div>
 @ <div class="footer">
 @ Fossil version $manifest_version $manifest_date
 @ </div>
 @ </body></html>
 @ ');
@@ -653,11 +658,12 @@
 
 /*
 ** Gradients and rounded corners.
 */
 static const char zBuiltinSkin4[] = 
-@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
+@ REPLACE INTO config(name,mtime,value)
+@ VALUES('css',now(),'/* General settings for the entire page */
 @ html {
 @   min-height: 100%;
 @ }
 @ body {
 @   margin: 0ex 1ex;
@@ -880,11 +886,11 @@
 @ }
 @ 
 @ textarea {
 @   font-size: 1em;
 @ }');
-@ REPLACE INTO config VALUES('header','<html>
+@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
 @ <head>
 @ <title>$<project_name>: $<title></title>
 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
 @       href="$home/timeline.rss">
 @ <link rel="stylesheet" href="$home/style.css?black2" type="text/css"
@@ -934,11 +940,11 @@
 @   html "<a href=''$home/login''>Login</a>"
 @ }
 @ </th1></ul></div>
 @ <div id="container">
 @ ');
-@ REPLACE INTO config VALUES('footer','</div>
+@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),'</div>
 @ <div class="footer">
 @ Fossil version $manifest_version $manifest_date
 @ </div>
 @ </body></html>
 @ ');
@@ -984,17 +990,20 @@
 ** Memory to hold the returned string is obtained from malloc.
 */
 static char *getSkin(int useDefault){
   Blob val;
   blob_zero(&val);
-  blob_appendf(&val, "REPLACE INTO config VALUES('css',%Q);\n",
+  blob_appendf(&val,
+     "REPLACE INTO config(name,value,mtime) VALUES('css',%Q,now());\n",
      useDefault ? zDefaultCSS : db_get("css", (char*)zDefaultCSS)
   );
-  blob_appendf(&val, "REPLACE INTO config VALUES('header',%Q);\n",
+  blob_appendf(&val,
+     "REPLACE INTO config(name,value,mtime) VALUES('header',%Q,now());\n",
      useDefault ? zDefaultHeader : db_get("header", (char*)zDefaultHeader)
   );
-  blob_appendf(&val, "REPLACE INTO config VALUES('footer',%Q);\n",
+  blob_appendf(&val,
+     "REPLACE INTO config(name,value,mtime) VALUES('footer',%Q,now());\n",
      useDefault ? zDefaultFooter : db_get("footer", (char*)zDefaultFooter)
   );
   return blob_str(&val);
 }
 
@@ -1048,11 +1057,11 @@
     if( db_exists("SELECT 1 FROM config WHERE name=%Q", zName)
           || strcmp(zName, "Default")==0 ){
       zErr = mprintf("Skin name \"%h\" already exists. "
                      "Choose a different name.", P("sn"));
     }else{
-      db_multi_exec("INSERT INTO config VALUES(%Q,%Q)",
+      db_multi_exec("INSERT INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
          zName, zCurrent
       );
     }
   }
 
@@ -1069,13 +1078,13 @@
       seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
                        " AND value=%Q", zCurrent);
     }
     if( !seen ){
       db_multi_exec(
-        "INSERT INTO config VALUES("
+        "INSERT INTO config(name,value,mtime) VALUES("
         "  strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
-        "  %Q)", zCurrent
+        "  %Q,now())", zCurrent
       );
     }
     seen = 0;
     for(i=0; i<sizeof(aBuiltinSkin)/sizeof(aBuiltinSkin[0]); i++){
       if( strcmp(aBuiltinSkin[i].zName, z)==0 ){

Index: src/user.c
==================================================================
--- src/user.c
+++ src/user.c
@@ -204,12 +204,12 @@
     }else{
       prompt_for_password("password: ", &passwd, 1);
     }
     zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
     db_multi_exec(
-      "INSERT INTO user(login,pw,cap,info)"
-      "VALUES(%B,%Q,%B,%B)",
+      "INSERT INTO user(login,pw,cap,info,mtime)"
+      "VALUES(%B,%Q,%B,%B,now())",
       &login, zPw, &caps, &contact
     );
     free(zPw);
   }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
     user_select();
@@ -249,11 +249,12 @@
     }
     if( blob_size(&pw)==0 ){
       printf("password unchanged\n");
     }else{
       char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
-      db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid);
+      db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
+                    zSecret, uid);
       free(zSecret);
     }
   }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
     int uid;
     if( g.argc!=4 && g.argc!=5 ){
@@ -263,12 +264,12 @@
     if( uid==0 ){
       fossil_fatal("no such user: %s", g.argv[3]);
     }
     if( g.argc==5 ){
       db_multi_exec(
-        "UPDATE user SET cap=%Q WHERE uid=%d", g.argv[4],
-        uid
+        "UPDATE user SET cap=%Q, mtime=now() WHERE uid=%d",
+        g.argv[4], uid
       );
     }
     printf("%s\n", db_text(0, "SELECT cap FROM user WHERE uid=%d", uid));
   }else{
     fossil_panic("user subcommand should be one of: "
@@ -340,12 +341,12 @@
     db_finalize(&s);
   }
 
   if( g.userUid==0 ){
     db_multi_exec(
-      "INSERT INTO user(login, pw, cap, info)"
-      "VALUES('anonymous', '', 'cfghjkmnoqw', '')"
+      "INSERT INTO user(login, pw, cap, info, mtime)"
+      "VALUES('anonymous', '', 'cfghjkmnoqw', '', now())"
     );
     g.userUid = db_last_insert_rowid();
     g.zLogin = "anonymous";
   }
 }
@@ -364,11 +365,11 @@
   if( g.argc!=3 ) usage("REPOSITORY");
   db_open_repository(g.argv[2]);
   sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0,
                           sha1_shared_secret_sql_function, 0, 0);
   db_multi_exec(
-    "UPDATE user SET pw=shared_secret(pw,login)"
+    "UPDATE user SET pw=shared_secret(pw,login), mtime=now()"
     " WHERE length(pw)>0 AND length(pw)!=40"
   );
 }
 
 /*

Index: src/xfer.c
==================================================================
--- src/xfer.c
+++ src/xfer.c
@@ -454,11 +454,11 @@
          "  (SELECT uuid FROM delta, blob"
          "    WHERE delta.rid=:rid AND delta.srcid=blob.rid)"
     " FROM blob"
     " WHERE rid=:rid"
     "   AND size>=0"
-    "   AND uuid NOT IN shun"
+    "   AND NOT EXISTS(SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)"
   );
   db_bind_int(&q1, ":rid", rid);
   rc = db_step(&q1);
   if( rc==SQLITE_ROW ){
     zUuid = db_column_text(&q1, 0);
@@ -738,13 +738,16 @@
   }
   db_finalize(&q);
 }
 
 /*
-** Send a single config card for configuration item zName
+** Send a single old-style config card for configuration item zName.
+**
+** This routine and the functionality it implements is scheduled for
+** removal on 2012-05-01.
 */
-static void send_config_card(Xfer *pXfer, const char *zName){
+static void send_legacy_config_card(Xfer *pXfer, const char *zName){
   if( zName[0]!='@' ){
     Blob val;
     blob_zero(&val);
     db_blob(&val, "SELECT value FROM config WHERE name=%Q", zName);
     if( blob_size(&val)>0 ){
@@ -760,11 +763,10 @@
     blob_appendf(pXfer->pOut, "config %s %d\n%s\n", zName,
                  blob_size(&content), blob_str(&content));
     blob_reset(&content);
   }
 }
-
 
 /*
 ** Called when there is an attempt to transfer private content to and
 ** from a server without authorization.
 */
@@ -1007,11 +1009,11 @@
     */
     if( blob_eq(&xfer.aToken[0], "login")
      && xfer.nToken==4
     ){
       if( disableLogin ){
-        g.okRead = g.okWrite = g.okPrivate = 1;
+        g.okRead = g.okWrite = g.okPrivate = g.okAdmin = 1;
       }else{
         if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
          || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
         ){
           cgi_reset_content();
@@ -1029,12 +1031,19 @@
     if( blob_eq(&xfer.aToken[0], "reqconfig")
      && xfer.nToken==2
     ){
       if( g.okRead ){
         char *zName = blob_str(&xfer.aToken[1]);
-        if( configure_is_exportable(zName) ){
-          send_config_card(&xfer, zName);
+        if( zName[0]=='/' ){
+          /* New style configuration transfer */
+          int groupMask = configure_name_to_mask(&zName[1], 0);
+          if( !g.okAdmin ) groupMask &= ~CONFIGSET_USER;
+          if( !g.okRdAddr ) groupMask &= ~CONFIGSET_ADDR;
+          configure_send_group(xfer.pOut, groupMask, 0);
+        }else if( configure_is_exportable(zName) ){
+          /* Old style configuration transfer */
+          send_legacy_config_card(&xfer, zName);
         }
       }
     }else
     
     /*   config NAME SIZE \n CONTENT
@@ -1052,11 +1061,11 @@
         cgi_reset_content();
         @ error not\sauthorized\sto\spush\sconfiguration
         nErr++;
         break;
       }
-      if( !recvConfig ){
+      if( !recvConfig && zName[0]=='@' ){
         configure_prepare_to_receive(0);
         recvConfig = 1;
       }
       configure_receive(zName, &content, CONFIGSET_ALL);
       blob_reset(&content);
@@ -1185,14 +1194,14 @@
 **     gdb fossil
 **     r test-xfer out.txt
 */
 void cmd_test_xfer(void){
   int notUsed;
+  db_find_and_open_repository(0,0);
   if( g.argc!=2 && g.argc!=3 ){
     usage("?MESSAGEFILE?");
   }
-  db_must_be_within_tree();
   blob_zero(&g.cgiIn);
   blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2]);
   disableLogin = 1;
   page_xfer();
   printf("%s\n", cgi_extract_content(&notUsed));
@@ -1331,25 +1340,32 @@
       while( zName ){
         blob_appendf(&send, "reqconfig %s\n", zName);
         zName = configure_next_name(configRcvMask);
         nCardSent++;
       }
-      if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){
-        configure_prepare_to_receive(0);
+      if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0
+       && (configRcvMask & CONFIGSET_OLDFORMAT)!=0
+      ){
+        int overwrite = (configRcvMask & CONFIGSET_OVERWRITE)!=0;
+        configure_prepare_to_receive(overwrite);
       }
       origConfigRcvMask = configRcvMask;
       configRcvMask = 0;
     }
 
     /* Send configuration parameters being pushed */
     if( configSendMask ){
-      const char *zName;
-      zName = configure_first_name(configSendMask);
-      while( zName ){
-        send_config_card(&xfer, zName);
-        zName = configure_next_name(configSendMask);
-        nCardSent++;
+      if( configSendMask & CONFIGSET_OLDFORMAT ){
+        const char *zName;
+        zName = configure_first_name(configSendMask);
+        while( zName ){
+          send_legacy_config_card(&xfer, zName);
+          zName = configure_next_name(configSendMask);
+          nCardSent++;
+        }
+      }else{
+        nCardSent += configure_send_group(xfer.pOut, configSendMask, 0);
       }
       configSendMask = 0;
     }
 
     /* Append randomness to the end of the message.  This makes all
@@ -1647,11 +1663,13 @@
         break;
       }
       blobarray_reset(xfer.aToken, xfer.nToken);
       blob_reset(&xfer.line);
     }
-    if( origConfigRcvMask & (CONFIGSET_TKT|CONFIGSET_USER) ){
+    if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0
+     && (configRcvMask & CONFIGSET_OLDFORMAT)!=0
+    ){
       configure_finalize_receive();
     }
     origConfigRcvMask = 0;
     if( nCardRcvd>0 ){
       fossil_print(zValueFormat, "Received:",