Check-in [15652ff081]
Not logged in
Overview

SHA1 Hash:15652ff081973f686974992ab86ff9d40479b53a
Date: 2007-08-29 02:55:33
User: aku
Comment:Merged drh's fixes new features (xfer, timeline handling, javascript based timeline highlighting) into my branch.
Timelines: ancestors | descendants | both | trunk
Other Links: files | ZIP archive | manifest

Tags And Properties
Changes
[hide diffs]

Modified ideas.txt from [429a4a9a12] to [ff113a4540].

@@ -1,28 +1,27 @@
 Random thoughts:
 
   *  Changes to manifest to support:
-
      +  Trees of wiki pages and tickets
      +  The ability to cap or close a branch
+     +  See "Extended Manifests" below
 
   *  Add the concept of "clusters" to speed the transfer of "tips"
      on a sync.
 
   *  Auxiliary tables:
-
      +  tip
      +  phantom
      +  mlink
      +  plink
      +  branch
      +  tree
 
   * Plink.isprim changed to record:
-     +  child is the principal descendent of parent.
-     +  child is a branch from parent
-     +  child uses parent as a merge
+     +  child is the principal descendent of parent. (1)
+     +  child is a branch from parent (2)
+     +  child uses parent as a merge (0)
 
   * tree records
      + type  (code, wiki, ticket)
      + name  (for wiki and ticket only)
      + treeid
@@ -38,6 +37,87 @@
   * website can toggle isprim between principal and branch.
      + How to preserve across rebuild.  A new record type?
      + How to share with other repositories
   * isprim guessed using userid of parent and child.  Change
     in id suggests a branch.  Same id suggests principal.
-    For a tie, go with the earliest check-in as the principal
+    For a tie, go with the earliest check-in as the principal'
+
+  * Autosync mode
+     + Set a preferred remote repository to use as a server
+        =  Clone repository is the default
+     + On commit, first pull.  If commit baseline is not a tip
+       prompt user to cancel or branch.  Default is cancel.
+     + Push after commit
+     + Automatically pull prior to update.
+     + Need an "undo" capability
+     + Designed to avoid branching in highly collaborative
+       environments.
+
+  * Archeological webpage improvements:
+     + Use a small amount of CSS+javascript on timelines so that
+       branching structure is displayed on mouseover.  On mouseover
+       of a checkin, highlight other checkins that are direct (non-merge)
+       descendents and ancestors of the mouseover checkin.
+     + Timeline showing individual branches
+     + Timeline shows forks and merges
+     + Tags shown on timeline (maybe) and in vinfo (surely).
+
+Extended manifests.
+  * normal manifest has:
+       C comment
+       D date-time
+       F* filename uuid
+       P uuid ...           -- omitted for first manifest
+       R repository-md5sum
+       U user-login
+       Z manifest-checksum
+  * Change the comment on a version:   -- always a leaf except in cluster
+       D date-time
+       E new-comment
+       P uuid              -- baseline whose comment is changed
+       U user-login
+       Z checksum
+       -- most recent wins
+  * Wiki edit
+       A* name uuid   -- zero or more attachments
+       C? comment
+       D date-time
+       N name         -- name of the wiki page
+       P uuid ...     -- omit for new wiki
+       U user-login
+       W uuid         -- The content file
+       Z manifest-cksum
+  * Ticket edit
+       A* name uuid   -- zero or more attachments
+       D date-time
+       N name         -- name of the ticket
+       P uuid         -- omit for new ticket
+       T uuid         -- content of the ticket
+       U user-login
+       Z manifest-cksum
+  * Set or erase a tag    -- most recent date wins
+       B* (+|-)tag uuid
+       C? comment
+       D date-time
+       V* (+|-) tag uuid    -- + to set, - to clear.
+       Z manifest-cksum
+       -- Must have at least one B or V.
+       -- Tag "hidden" means do not sync
+       -- Tag "closed" means do not display as a leaf
+  * A cluster
+       M+ uuid
+       Z manifest-cksum
+  * Complete set of cards in a manifest files:
+       A filename uuid
+       B (+|-)branch-tag uuid
+       C comment
+       D date-time
+       E edited-comment
+       F filename uuid
+       N name
+       P uuid ...
+       R repository-md5sum
+       T uuid
+       U user-login
+       V (+|-)version-tag uuid
+       W uuid
+       Z manifest-checksum

Modified src/descendents.c from [5d5c8b9dec] to [cea178cd6b].

@@ -130,17 +130,23 @@
   login_check_credentials();
   if( !g.okRead ){ login_needed(); return; }
 
   style_header("Leaves");
   db_prepare(&q,
-    "SELECT blob.uuid, datetime(event.mtime,'localtime'),"
-    "       event.comment, event.user"
+    "SELECT blob.rid, blob.uuid, datetime(event.mtime,'localtime'),"
+    "       event.comment, event.user, 1, 1, 0"
     "  FROM blob, event"
     " WHERE blob.rid IN"
     "       (SELECT cid FROM plink EXCEPT SELECT pid FROM plink)"
     "   AND event.objid=blob.rid"
     " ORDER BY event.mtime DESC"
   );
-  www_print_timeline(&q, 0);
+  www_print_timeline(&q, 0, 0, 0);
   db_finalize(&q);
+  @ <script>
+  @ function xin(id){
+  @ }
+  @ function xout(id){
+  @ }
+  @ </script>
   style_footer();
 }

Modified src/manifest.c from [02dabff247] to [bf7dfee62a].

@@ -301,28 +301,30 @@
 
   if( manifest_parse(&m, pContent)==0 ){
     return 0;
   }
   db_begin_transaction();
-  for(i=0; i<m.nParent; i++){
-    int pid = uuid_to_rid(m.azParent[i], 1);
-    db_multi_exec("INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime)"
-                  "VALUES(%d, %d, %d, %.17g)", pid, rid, i==0, m.rDate);
-    if( i==0 ){
-      add_mlink(pid, 0, rid, &m);
+  if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
+    for(i=0; i<m.nParent; i++){
+      int pid = uuid_to_rid(m.azParent[i], 1);
+      db_multi_exec("INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime)"
+                    "VALUES(%d, %d, %d, %.17g)", pid, rid, i==0, m.rDate);
+      if( i==0 ){
+        add_mlink(pid, 0, rid, &m);
+      }
+    }
+    db_prepare(&q, "SELECT cid FROM plink WHERE pid=%d AND isprim", rid);
+    while( db_step(&q)==SQLITE_ROW ){
+      int cid = db_column_int(&q, 0);
+      add_mlink(rid, &m, cid, 0);
     }
-  }
-  db_prepare(&q, "SELECT cid FROM plink WHERE pid=%d AND isprim", rid);
-  while( db_step(&q)==SQLITE_ROW ){
-    int cid = db_column_int(&q, 0);
-    add_mlink(rid, &m, cid, 0);
-  }
-  db_finalize(&q);
-  db_multi_exec(
-    "INSERT INTO event(type,mtime,objid,user,comment)"
-    "VALUES('ci',%.17g,%d,%Q,%Q)",
-    m.rDate, rid, m.zUser, m.zComment
-  );
+    db_finalize(&q);
+    db_multi_exec(
+      "INSERT INTO event(type,mtime,objid,user,comment)"
+      "VALUES('ci',%.17g,%d,%Q,%Q)",
+      m.rDate, rid, m.zUser, m.zComment
+    );
+  }
   db_end_transaction(0);
   manifest_clear(&m);
   return 1;
 }

Modified src/timeline.c from [7640f70a07] to [a0b837b0fb].

@@ -39,10 +39,26 @@
     @ <b>[%s(zShortUuid)]</b>
   }
 }
 
 /*
+** Generate a hyperlink that invokes javascript to highlight
+** a version on mouseover.
+*/
+void hyperlink_to_uuid_with_highlight(const char *zUuid, int id){
+  char zShortUuid[UUID_SIZE+1];
+  sprintf(zShortUuid, "%.10s", zUuid);
+  if( g.okHistory ){
+    @ <a onmouseover='hilite("m%d(id)")' onmouseout='unhilite("m%d(id)")'
+    @    href="%s(g.zBaseURL)/vinfo/%s(zUuid)">[%s(zShortUuid)]</a>
+  }else{
+    @ <b onmouseover='hilite("m%d(id)")' onmouseout='unhilite("m%d(id)")'>
+    @ [%s(zShortUuid)]</b>
+  }
+}
+
+/*
 ** Generate a hyperlink to a diff between two versions.
 */
 void hyperlink_to_diff(const char *zV1, const char *zV2){
   if( g.okHistory ){
     if( zV2==0 ){
@@ -55,21 +71,37 @@
 
 /*
 ** Output a timeline in the web format given a query.  The query
 ** should return 4 columns:
 **
-**    0.  UUID
-**    1.  Date/Time
-**    2.  Comment string
-**    3.  User
+**    0.  rid
+**    1.  UUID
+**    2.  Date/Time
+**    3.  Comment string
+**    4.  User
+**    5.  Number of non-merge children
+**    6.  Number of parents
+**    7.  True if is a leaf
 */
-void www_print_timeline(Stmt *pQuery, char *zLastDate){
+void www_print_timeline(
+  Stmt *pQuery,
+  char *zLastDate,
+  int (*xCallback)(int, Blob*),
+  Blob *pArg
+ ){
   char zPrevDate[20];
   zPrevDate[0] = 0;
   @ <table cellspacing=0 border=0 cellpadding=0>
   while( db_step(pQuery)==SQLITE_ROW ){
-    const char *zDate = db_column_text(pQuery, 1);
+    int rid = db_column_int(pQuery, 0);
+    int nPChild = db_column_int(pQuery, 5);
+    int nParent = db_column_int(pQuery, 6);
+    int isLeaf = db_column_int(pQuery, 7);
+    const char *zDate = db_column_text(pQuery, 2);
+    if( xCallback ){
+      xCallback(rid, pArg);
+    }
     if( memcmp(zDate, zPrevDate, 10) ){
       sprintf(zPrevDate, "%.10s", zDate);
       @ <tr><td colspan=3>
       @ <table cellpadding=2 border=0>
       @ <tr><td bgcolor="#a0b5f4" class="border1">
@@ -77,30 +109,96 @@
       @ <td bgcolor="#d0d9f4" class="bkgnd1">%s(zPrevDate)</td>
       @ </tr></table>
       @ </td></tr></table>
       @ </td></tr>
     }
-    @ <tr><td valign="top">%s(&zDate[11])</td>
+    @ <tr id="m%d(rid)" onmouseover='xin("m%d(rid)")'
+    @     onmouseout='xout("m%d(rid)")'>
+    @ <td valign="top">%s(&zDate[11])</td>
     @ <td width="20"></td>
     @ <td valign="top" align="left">
-    hyperlink_to_uuid(db_column_text(pQuery,0));
-    @ %h(db_column_text(pQuery,2)) (by %h(db_column_text(pQuery,3)))</td>
+    hyperlink_to_uuid(db_column_text(pQuery,1));
+    @ %h(db_column_text(pQuery,3))
+    if( nParent>1 ){
+      Stmt q;
+      @ <b>Merge</b> from
+      db_prepare(&q,
+        "SELECT rid, uuid FROM plink, blob"
+        " WHERE plink.cid=%d AND blob.rid=plink.pid AND plink.isprim=0",
+        rid
+      );
+      while( db_step(&q)==SQLITE_ROW ){
+        int mrid = db_column_int(&q, 0);
+        const char *zUuid = db_column_text(&q, 1);
+        hyperlink_to_uuid_with_highlight(zUuid, mrid);
+      }
+      db_finalize(&q);
+    }
+    if( nPChild>1 ){
+      Stmt q;
+      @ <b>Fork</b> to
+      db_prepare(&q,
+        "SELECT rid, uuid FROM plink, blob"
+        " WHERE plink.pid=%d AND blob.rid=plink.cid AND plink.isprim>0",
+        rid
+      );
+      while( db_step(&q)==SQLITE_ROW ){
+        int frid = db_column_int(&q, 0);
+        const char *zUuid = db_column_text(&q, 1);
+        hyperlink_to_uuid_with_highlight(zUuid, frid);
+      }
+      db_finalize(&q);
+    }
+    if( isLeaf ){
+      @ <b>Leaf</b>
+    }
+    @ (by %h(db_column_text(pQuery,4)))</td></tr>
     if( zLastDate ){
       strcpy(zLastDate, zDate);
     }
   }
   @ </table>
 }
 
+/*
+** Generate javascript code that records the parents and children
+** of the version rid.
+*/
+static int save_parentage_javascript(int rid, Blob *pOut){
+  const char *zSep;
+  Stmt q;
 
+  db_prepare(&q, "SELECT pid FROM plink WHERE cid=%d", rid);
+  zSep = "";
+  blob_appendf(pOut, "parentof[\"m%d\"] = [", rid);
+  while( db_step(&q)==SQLITE_ROW ){
+    int pid = db_column_int(&q, 0);
+    blob_appendf(pOut, "%s\"m%d\"", zSep, pid);
+    zSep = ",";
+  }
+  db_finalize(&q);
+  blob_appendf(pOut, "];\n");
+  db_prepare(&q, "SELECT cid FROM plink WHERE pid=%d", rid);
+  zSep = "";
+  blob_appendf(pOut, "childof[\"m%d\"] = [", rid);
+  while( db_step(&q)==SQLITE_ROW ){
+    int pid = db_column_int(&q, 0);
+    blob_appendf(pOut, "%s\"m%d\"", zSep, pid);
+    zSep = ",";
+  }
+  db_finalize(&q);
+  blob_appendf(pOut, "];\n");
+  return 0;
+}
 
 /*
 ** WEBPAGE: timeline
 */
 void page_timeline(void){
   Stmt q;
   char *zSQL;
+  Blob scriptInit;
   char zDate[100];
   const char *zStart = P("d");
   int nEntry = atoi(PD("n","25"));
 
   /* To view the timeline, must have permission to read project data.
@@ -115,11 +213,14 @@
                 "   AND cap LIKE '%%h%%'") ){
     @ <p><b>Note:</b> You will be able to access <u>much</u> more
     @ historical information if <a href="%s(g.zBaseURL)/login">login</a>.</p>
   }
   zSQL = mprintf(
-    "SELECT uuid, datetime(event.mtime,'localtime'), comment, user"
+    "SELECT blob.rid, uuid, datetime(event.mtime,'localtime'), comment, user,"
+    "       (SELECT count(*) FROM plink WHERE pid=blob.rid AND isprim=1),"
+    "       (SELECT count(*) FROM plink WHERE cid=blob.rid),"
+    "       NOT EXISTS (SELECT 1 FROM plink WHERE pid=blob.rid)"
     "  FROM event, blob"
     " WHERE event.type='ci' AND blob.rid=event.objid"
   );
   if( zStart ){
     while( isspace(zStart[0]) ){ zStart++; }
@@ -130,15 +231,75 @@
   }
   zSQL = mprintf("%z ORDER BY event.mtime DESC LIMIT %d", zSQL, nEntry);
   db_prepare(&q, zSQL);
   free(zSQL);
   zDate[0] = 0;
-  www_print_timeline(&q, zDate);
+  blob_zero(&scriptInit);
+  www_print_timeline(&q, zDate, save_parentage_javascript, &scriptInit);
   db_finalize(&q);
   if( zStart==0 ){
     zStart = zDate;
   }
+  @ <script>
+  @ var parentof = new Object();
+  @ var childof = new Object();
+  cgi_append_content(blob_buffer(&scriptInit), blob_size(&scriptInit));
+  blob_reset(&scriptInit);
+  @ function setall(value){
+  @   for(var x in parentof){
+  @     setone(x,value);
+  @   }
+  @ }
+  @ function setone(id, onoff){
+  @   if( parentof[id]==null ) return 0;
+  @   var w = document.getElementById(id);
+  @   var clr = onoff==1 ? "#e0e0ff" : "#ffffff";
+  @   if( w.backgroundColor==clr ){
+  @     return 0
+  @   }else{
+  @     w.style.backgroundColor = clr
+  @     return 1
+  @   }
+  @ }
+  @ function xin(id) {
+  @   setall(0);
+  @   setone(id,1);
+  @   set_children(id);
+  @   set_parents(id);
+  @ }
+  @ function xout(id) {
+  @   setall(0);
+  @ }
+  @ function set_parents(id){
+  @   var plist = parentof[id];
+  @   if( plist==null ) return;
+  @   for(var x in plist){
+  @     var pid = plist[x];
+  @     if( setone(pid,1)==1 ){
+  @       set_parents(pid);
+  @     }
+  @   }
+  @ }
+  @ function set_children(id){
+  @   var clist = childof[id];
+  @   if( clist==null ) return;
+  @   for(var x in clist){
+  @     var cid = clist[x];
+  @     if( setone(cid,1)==1 ){
+  @       set_children(cid);
+  @     }
+  @   }
+  @ }
+  @ function hilite(id) {
+  @   var x = document.getElementById(id);
+  @   x.style.color = "#ff0000";
+  @ }
+  @ function unhilite(id) {
+  @   var x = document.getElementById(id);
+  @   x.style.color = "#000000";
+  @ }
+  @ </script>
   @ <hr>
   @ <form method="GET" action="%s(g.zBaseURL)/timeline">
   @ Start Date:
   @ <input type="text" size="30" value="%h(zStart)" name="d">
   @ Number Of Entries:

Modified src/vfile.c from [fdfe66efd6] to [aea4942364].

@@ -55,10 +55,29 @@
   }
   return rid;
 }
 
 /*
+** Verify that an object is not a phantom.  If the object is
+** a phantom, output an error message and quick.
+*/
+void vfile_verify_not_phantom(int rid, const char *zFilename){
+  if( db_int(-1, "SELECT size FROM blob WHERE rid=%d", rid)<0 ){
+    if( zFilename ){
+      fossil_fatal("content missing for %s", zFilename);
+    }else{
+      char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
+      if( zUuid ){
+        fossil_fatal("content missing for [%.10s]", zUuid);
+      }else{
+        fossil_panic("bad object id: %d", rid);
+      }
+    }
+  }
+}
+
+/*
 ** Build a catalog of all files in a baseline.
 ** We scan the baseline file for lines of the form:
 **
 **     F NAME UUID
 **
@@ -69,10 +88,11 @@
   char *zName, *zUuid;
   Stmt ins;
   Blob line, token, name, uuid;
   int seenHeader = 0;
   db_begin_transaction();
+  vfile_verify_not_phantom(vid, 0);
   db_multi_exec("DELETE FROM vfile WHERE vid=%d", vid);
   db_prepare(&ins,
     "INSERT INTO vfile(vid,rid,mrid,pathname) "
     " VALUES(:vid,:id,:id,:name)");
   db_bind_int(&ins, ":vid", vid);
@@ -90,10 +110,11 @@
     if( blob_token(&line, &uuid)==0 ) break;
     zName = blob_str(&name);
     defossilize(zName);
     zUuid = blob_str(&uuid);
     rid = uuid_to_rid(zUuid, 0);
+    vfile_verify_not_phantom(rid, zName);
     if( rid>0 && file_is_simple_pathname(zName) ){
       db_bind_int(&ins, ":id", rid);
       db_bind_text(&ins, ":name", zName);
       db_step(&ins);
       db_reset(&ins);

Modified src/xfer.c from [caacb8871c] to [f732b233d1].

@@ -63,10 +63,17 @@
     rid = content_put(0, blob_str(pUuid), 0);
   }
   return rid;
 }
 
+/*
+** Remember that the other side of the connection already has a copy
+** of the file rid.
+*/
+static void remote_has(int rid){
+  db_multi_exec("INSERT OR IGNORE INTO onremote VALUES(%d)", rid);
+}
 
 /*
 ** The aToken[0..nToken-1] blob array is a parse of a "file" line
 ** message.  This routine finishes parsing that message and does
 ** a record insert of the file.
@@ -126,10 +133,11 @@
   if( rid==0 ){
     blob_appendf(&pXfer->err, "%s", g.zErrMsg);
   }else{
     manifest_crosslink(rid, &content);
   }
+  remote_has(rid);
 }
 
 /*
 ** Try to send a file as a delta.  If successful, return the number
 ** of bytes in the delta.  If not, return zero.
@@ -221,21 +229,46 @@
     blob_append(pXfer->pOut, blob_buffer(&content), size);
     pXfer->nFileSent++;
   }else{
     pXfer->nDeltaSent++;
   }
-  db_multi_exec("INSERT INTO onremote VALUES(%d)", rid);
+  remote_has(rid);
   blob_reset(&uuid);
 }
 
 /*
+** Send the file identified by mid and pUuid.  If that file happens
+** to be a manifest, then also send all of the associated content
+** files for that manifest.  If the file is not a manifest, then this
+** routine is the equivalent of send_file().
+*/
+static void send_manifest(Xfer *pXfer, int mid, Blob *pUuid, int srcId){
+  Stmt q2;
+  send_file(pXfer, mid, pUuid, srcId);
+  db_prepare(&q2,
+     "SELECT pid, uuid, fid FROM mlink, blob"
+     " WHERE rid=fid AND mid=%d",
+     mid
+  );
+  while( db_step(&q2)==SQLITE_ROW ){
+    int pid, fid;
+    Blob uuid;
+    pid = db_column_int(&q2, 0);
+    db_ephemeral_blob(&q2, 1, &uuid);
+    fid = db_column_int(&q2, 2);
+    send_file(pXfer, fid, &uuid, pid);
+  }
+  db_finalize(&q2);
+}
+
+/*
 ** This routine runs when either client or server is notified that
-** the other side things rid is a leaf manifest.  If we hold
+** the other side thinks rid is a leaf manifest.  If we hold
 ** children of rid, then send them over to the other side.
 */
 static void leaf_response(Xfer *pXfer, int rid){
-  Stmt q1, q2;
+  Stmt q1;
   db_prepare(&q1,
       "SELECT cid, uuid FROM plink, blob"
       " WHERE blob.rid=plink.cid"
       "   AND plink.pid=%d",
       rid
@@ -244,24 +277,11 @@
     Blob uuid;
     int cid;
 
     cid = db_column_int(&q1, 0);
     db_ephemeral_blob(&q1, 1, &uuid);
-    send_file(pXfer, cid, &uuid, rid);
-    db_prepare(&q2,
-       "SELECT pid, uuid, fid FROM mlink, blob"
-       " WHERE rid=fid AND mid=%d",
-       cid
-    );
-    while( db_step(&q2)==SQLITE_ROW ){
-      int pid, fid;
-      pid = db_column_int(&q2, 0);
-      db_ephemeral_blob(&q2, 1, &uuid);
-      fid = db_column_int(&q2, 2);
-      send_file(pXfer, fid, &uuid, pid);
-    }
-    db_finalize(&q2);
+    send_manifest(pXfer, cid, &uuid, rid);
     if( blob_size(pXfer->pOut)<pXfer->mxSend ){
       leaf_response(pXfer, cid);
     }
   }
 }
@@ -278,10 +298,37 @@
   while( db_step(&q)==SQLITE_ROW ){
     const char *zUuid = db_column_text(&q, 0);
     blob_appendf(pXfer->pOut, "leaf %s\n", zUuid);
   }
   db_finalize(&q);
+}
+
+/*
+** Sent leaf content for every leaf that is not found in the
+** onremote table.  This is intended to send leaf content for
+** every leaf that is unknown on the remote end.
+**
+** In addition, we might send "igot" messages for a few generations of
+** parents of the unknown leaves.  This will speed the transmission
+** of new branches.
+*/
+static void send_unknown_leaf_content(Xfer *pXfer){
+  Stmt q1;
+  db_prepare(&q1,
+    "SELECT rid, uuid FROM blob WHERE rid IN"
+    "  (SELECT cid FROM plink EXCEPT SELECT pid FROM plink)"
+    "  AND NOT EXISTS(SELECT 1 FROM onremote WHERE rid=blob.rid)"
+  );
+  while( db_step(&q1)==SQLITE_ROW ){
+    Blob uuid;
+    int cid;
+
+    cid = db_column_int(&q1, 0);
+    db_ephemeral_blob(&q1, 1, &uuid);
+    send_manifest(pXfer, cid, &uuid, 0);
+  }
+  db_finalize(&q1);
 }
 
 /*
 ** Sen a gimme message for every phantom.
 */
@@ -407,27 +454,29 @@
       }
     }else
 
     /*   gimme UUID
     **
-    ** Client is requesting a file
+    ** Client is requesting a file.  If the file is a manifest,
+    ** the server can assume that the client also needs all content
+    ** files associated with that manifest.
     */
     if( blob_eq(&xfer.aToken[0], "gimme")
      && xfer.nToken==2
      && blob_is_uuid(&xfer.aToken[1])
     ){
       if( isPull ){
         int rid = rid_from_uuid(&xfer.aToken[1], 0);
         if( rid ){
-          send_file(&xfer, rid, &xfer.aToken[1], 0);
+          send_manifest(&xfer, rid, &xfer.aToken[1], 0);
         }
       }
     }else
 
     /*   igot UUID
     **
-    ** Client announces that it has a particular file
+    ** Client announces that it has a particular file.
     */
     if( xfer.nToken==2
      && blob_eq(&xfer.aToken[0], "igot")
      && blob_is_uuid(&xfer.aToken[1])
     ){
@@ -447,22 +496,26 @@
     if( xfer.nToken==2
      && blob_eq(&xfer.aToken[0], "leaf")
      && blob_is_uuid(&xfer.aToken[1])
     ){
       int rid = rid_from_uuid(&xfer.aToken[1], 0);
-      if( isPull && rid ){
-        leaf_response(&xfer, rid);
-      }
-      if( isPush && !rid ){
+      if( rid ){
+        remote_has(rid);
+        if( isPull ){
+          leaf_response(&xfer, rid);
+        }
+      }else if( isPush ){
         content_put(0, blob_str(&xfer.aToken[1]), 0);
       }
     }else
 
     /*    pull  SERVERCODE  PROJECTCODE
     **    push  SERVERCODE  PROJECTCODE
     **
-    ** The client wants either send or receive
+    ** The client wants either send or receive.  The server should
+    ** verify that the project code matches and that the server code
+    ** does not match.
     */
     if( xfer.nToken==3
      && (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push"))
      && blob_is_uuid(&xfer.aToken[1])
      && blob_is_uuid(&xfer.aToken[2])
@@ -537,10 +590,11 @@
     }else
 
     /*    login  USER  NONCE  SIGNATURE
     **
     ** Check for a valid login.  This has to happen before anything else.
+    ** The client can send multiple logins.  Permissions are cumulative.
     */
     if( blob_eq(&xfer.aToken[0], "login")
      && xfer.nToken==4
     ){
       if( disableLogin ){
@@ -558,10 +612,13 @@
     }
     blobarray_reset(xfer.aToken, xfer.nToken);
   }
   if( isPush ){
     request_phantoms(&xfer);
+  }
+  if( isPull ){
+    send_unknown_leaf_content(&xfer);
   }
   db_end_transaction(0);
 }
 
 /*
@@ -697,53 +754,67 @@
       xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
 
       /*   file UUID SIZE \n CONTENT
       **   file UUID DELTASRC SIZE \n CONTENT
       **
-      ** Receive a file transmitted from the other side
+      ** Receive a file transmitted from the server.
       */
       if( blob_eq(&xfer.aToken[0],"file") ){
         xfer_accept_file(&xfer);
       }else
 
       /*   gimme UUID
       **
-      ** Server is requesting a file
+      ** Server is requesting a file.  If the file is a manifest, assume
+      ** that the server will also want to know all of the content files
+      ** associated with the manifest and send those too.
       */
       if( blob_eq(&xfer.aToken[0], "gimme")
        && xfer.nToken==2
        && blob_is_uuid(&xfer.aToken[1])
       ){
         nMsg++;
         if( pushFlag ){
           int rid = rid_from_uuid(&xfer.aToken[1], 0);
-          send_file(&xfer, rid, &xfer.aToken[1], 0);
+          send_manifest(&xfer, rid, &xfer.aToken[1], 0);
         }
       }else
 
       /*   igot UUID
       **
-      ** Server announces that it has a particular file
+      ** Server announces that it has a particular file.  If this is
+      ** not a file that we have and we are pulling, then create a
+      ** phantom to cause this file to be requested on the next cycle.
+      ** Always remember that the server has this file so that we do
+      ** not transmit it by accident.
       */
       if( xfer.nToken==2
        && blob_eq(&xfer.aToken[0], "igot")
        && blob_is_uuid(&xfer.aToken[1])
       ){
+        int rid = 0;
         nMsg++;
         if( pullFlag ){
           if( !db_exists("SELECT 1 FROM blob WHERE uuid='%b' AND size>=0",
                 &xfer.aToken[1]) ){
-            content_put(0, blob_str(&xfer.aToken[1]), 0);
+            rid = content_put(0, blob_str(&xfer.aToken[1]), 0);
             newPhantom = 1;
           }
         }
+        if( rid==0 ){
+          rid = rid_from_uuid(&xfer.aToken[1], 0);
+        }
+        remote_has(rid);
       }else
 
 
       /*   leaf UUID
       **
-      ** Server announces that it has a particular manifest
+      ** Server announces that it has a particular manifest.  Send
+      ** any children of this leaf that we have if we are pushing.
+      ** Make the leaf a phantom if we are pulling.  Remember that the
+      ** remote end has the specified UUID.
       */
       if( xfer.nToken==2
        && blob_eq(&xfer.aToken[0], "leaf")
        && blob_is_uuid(&xfer.aToken[1])
       ){
@@ -751,19 +822,21 @@
         nMsg++;
         if( pushFlag && rid ){
           leaf_response(&xfer, rid);
         }
         if( pullFlag && rid==0 ){
-          content_put(0, blob_str(&xfer.aToken[1]), 0);
+          rid = content_put(0, blob_str(&xfer.aToken[1]), 0);
           newPhantom = 1;
         }
+        remote_has(rid);
       }else
 
 
       /*   push  SERVERCODE  PRODUCTCODE
       **
-      ** Should only happen in response to a clone.
+      ** Should only happen in response to a clone.  This message tells
+      ** the client what product to use for the new database.
       */
       if( blob_eq(&xfer.aToken[0],"push")
        && xfer.nToken==3
        && cloneFlag
        && blob_is_uuid(&xfer.aToken[1])