Login
Artifact [0c64fc5a07]
Login

Artifact 0c64fc5a073286a6956398351d70f141ee21e0e4:


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
   Copyright (c) 2014 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:
     drh@hwaci.com
     http://www.hwaci.com/drh/
  
  *****************************************************************************
   This file houses the code for checkin-level APIS.
*/
#include <assert.h>

#include "fossil-scm/fossil-internal.h"

/* Only for debugging */
#include <stdio.h>
#define MARKER(pfexp)                                               \
  do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__);   \
    printf pfexp;                                                   \
  } while(0)


/**
   Expects f to have an opened checkout. Assumes zRelName is a
   checkout-relative simple path. It loads the file's contents and
   stores them into the blob table. If rid is not NULL, *rid is
   assigned the blob.rid (possibly new, possilbly re-used!). If uuid
   is not NULL then *uuid is assigned to the content's UUID. The *uuid
   bytes are owned by the caller, who must eventually fsl_free()
   them. If content with the same UUID already exists, it does not get
   re-imported but rid/uuid will (if not NULL) contain the old values.

   If parentRid is >0 then it must refer to the previous version of
   zRelName's content. The parent version gets deltified vs the new
   one. Note that deltification is a suggestion which the library will
   ignore if (e.g.) the parent content is already a delta of something
   else.

   The wise caller will have a transaction in place when calling this.

   Returns 0 on success. On error rid and uuid are not modified.
*/
static int fsl_checkin_import_file( fsl_cx * f, char const * zRelName,
                                    fsl_id_t parentRid,
                                    fsl_id_t *rid, fsl_uuid_str * uuid ){
  fsl_buffer * nbuf = &f->scratch;
  fsl_size_t const oldSize = nbuf->used;
  fsl_buffer * fbuf = &f->fileContent;
  char const * fn;
  int rc;
  fsl_id_t fnid = 0;
  fsl_id_t rcRid = 0;
  assert(!fbuf->used && "Misuse of f->fileContent");
  assert(f->ckout.dir);
  rc = fsl_repo_filename_fnid2(f, zRelName, &fnid, 1);
  if(rc) return rc;
  assert(fnid>0);

  rc = fsl_buffer_appendf(nbuf, "%s%s", f->ckout.dir, zRelName);
  nbuf->used = oldSize;
  if(rc) goto end;
  fn = fsl_buffer_cstr(nbuf) + oldSize;
  rc = fsl_buffer_fill_from_filename( fbuf, fn );
  if(rc){
    fsl_cx_err_set(f, rc, "Error %s importing file: %s",
                   fsl_rc_cstr(rc), fn);
    goto end;
  }

  rc = fsl_content_put( f, fbuf, &rcRid );
  if(!rc){
    assert(rcRid > 0);
    if(parentRid>0){
      rc = fsl_content_deltify(f, parentRid, rcRid, 0);
    }
    if(!rc){
      if(rid) *rid = rcRid;
      if(uuid){
        *uuid = fsl_rid_to_uuid(f, rcRid);
        if(!*uuid) rc = (f->error.code ? f->error.code : FSL_RC_OOM);
      }
    }
  }
  end:
  fsl_cx_yield_file_buffer(f);
  assert(0==fbuf->used);
  return rc;
}


int fsl_filename_to_vfile_ids( fsl_cx * f, fsl_id_t vid,
                               fsl_id_bag * dest, char const * zName){
  fsl_stmt st = fsl_stmt_empty;
  fsl_db * db = fsl_needs_checkout(f);
  int rc;
  if(!db) return FSL_RC_NOT_A_CHECKOUT;
  else if(zName && *zName){
    char const * zCollation = fsl_cx_filename_collation(f);
    rc = fsl_db_prepare(db, &st,
                        "SELECT id FROM vfile WHERE vid=%"FSL_ID_T_PFMT
                        " AND (pathname=%Q %s "
                        "OR (pathname>'%q/' %s AND pathname<'%q0' %s))",
                        (fsl_id_t)vid,
                        zName, zCollation, zName,
                        zCollation, zName, zCollation);
  }else{
    rc = fsl_db_prepare(db, &st,
                        "SELECT id FROM vfile WHERE vid=%"FSL_ID_T_PFMT,
                        (fsl_id_t)vid);
  }
  while(!rc && (FSL_RC_STEP_ROW == fsl_stmt_step(&st))){
    rc = fsl_id_bag_insert( dest, fsl_stmt_g_id(&st, 0) );
  }
  if(FSL_RC_STEP_DONE==rc) rc = 0;
  fsl_stmt_finalize(&st);
  if(rc && !f->error.code && db->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

int fsl_filename_to_vfile_id( fsl_cx * f, char const * zName, fsl_id_t * vfid ){
  fsl_stmt * st = NULL;
  fsl_db * db = fsl_needs_checkout(f);
  int rc;
  assert(db);
  if(!db) return FSL_RC_NOT_A_CHECKOUT;
  rc = fsl_db_prepare_cached(db, &st,
                             "SELECT id FROM vfile WHERE pathname=? %s",
                             fsl_cx_filename_collation(f));
  if(!rc){
    rc = fsl_stmt_bind_text(st, 1, zName, -1, 0);
    if(!rc && (FSL_RC_STEP_ROW == fsl_stmt_step(st))){
      *vfid = fsl_stmt_g_id(st, 0);
    }else{
      *vfid = 0;
    }
    fsl_stmt_cached_yield(st);
  }
  if(rc && !f->error.code && db->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

int fsl_checkin_file_enqueue(fsl_cx * f, char const * zName,
                             char relativeToCwd){
  if(!f || !zName || !*zName) return FSL_RC_MISUSE;
  else if(!fsl_needs_checkout(f)) return FSL_RC_NOT_A_CHECKOUT;
  else{
    fsl_buffer canon = fsl_buffer_empty;
    int rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &canon);
    if(!rc){
      fsl_id_t vfid = 0;
      char const * path = fsl_buffer_cstr(&canon);
      char dirCheck;
      fsl_buffer * buf = &f->fsScratch;
      fsl_size_t const oldBagSize = f->ckin.selectedIds.entryCount;
      assert(!buf->used && "Misuse of f->fsScratch");

      rc = fsl_filename_to_vfile_id(f, path, &vfid);
      if(rc) goto out;
      else if(vfid>0 && fsl_id_bag_contains(&f->ckin.selectedIds, vfid)){
        /* A single file matching a vfile entry... */
        rc = 0;
      }else{
        /* Try interpreting it as a directory... */
        while('/' == canon.mem[canon.used-1]){
          assert(canon.used);
          canon.mem[--canon.used] = 0;
          assert(canon.used>0);
        }
        rc = fsl_buffer_appendf(buf, "%s%b", f->ckout.dir, &canon);
        if(rc) goto out;
        dirCheck = fsl_dir_check(fsl_buffer_cstr(buf));
        rc = fsl_filename_to_vfile_ids(f, f->ckout.rid, &f->ckin.selectedIds, path);
        if(!rc && (dirCheck<=0)){
          /* TODO: distinguish between "fossil knows nothing about" and "file not found."
             Heuristic: if file exists but it's not in vfile, report FSL_RC_UNKNOWN_RESOURCE,
             otherwise FSL_RC_NOT_FOUND.
          */
          if( oldBagSize == f->ckin.selectedIds.entryCount ){
            char const * msg;
            if(dirCheck<0/* it's a non-directory entry in the filesystem */){
              rc = FSL_RC_UNKNOWN_RESOURCE /* it's presumably not in vfile */;
              msg = "Not found in repository db: %b";
            }else{
              rc = FSL_RC_NOT_FOUND;
              msg = "File not found: %b";
            }
            rc = fsl_cx_err_set(f, rc, msg, &canon);
          }
        }
      }
      out:
      buf->used = 0;
    }
    fsl_buffer_clear(&canon);
    return rc;
  }
}

/**
   Internal helper which assumes path is a subdirectory name
   or NULL/empty (meaning the whole repo). Dequeues all
   matching files.

   Returns 0 on success.
*/
static int fsl_checkin_dequeue_files(fsl_cx * f, char const * path){
  int rc;
  fsl_id_bag list = fsl_id_bag_empty;
  rc = fsl_filename_to_vfile_ids(f, f->ckout.rid, &list, path);
  if(!rc && list.entryCount){
    fsl_id_t nid;
    for( nid = fsl_id_bag_first(&list);
         nid;
         nid = fsl_id_bag_next(&list, nid)){
      fsl_id_bag_remove(&f->ckin.selectedIds, nid);
    }
  }
  fsl_id_bag_clear(&list);
  return rc;
}

int fsl_checkin_file_dequeue(fsl_cx * f, char const * zName,
                             char relativeToCwd){
  fsl_db * db;
  int rc;
  if(!f) return FSL_RC_MISUSE;
  else if(!(db = fsl_needs_checkout(f))) return FSL_RC_NOT_A_CHECKOUT;
  else if(!f->ckin.selectedIds.entryCount) return FSL_RC_NOT_FOUND;
  else if(zName && *zName){
    fsl_buffer canon = fsl_buffer_empty;
    char const * path;
    rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &canon);
    if(!rc){
      fsl_id_t vfid = 0;
      path = fsl_buffer_cstr(&canon);
      rc = fsl_filename_to_vfile_id(f, path, &vfid);
      /* FIXME: work on directories! */
      if(vfid/* && fsl_id_bag_contains(&f->ckin.selectedIds, vfid)*/){
        fsl_id_bag_remove(&f->ckin.selectedIds, vfid);
      }else if(!rc){
        /* Assume a directory/path (but we don't bother creating the absolute
           path to confirm that).
        */
        rc = fsl_checkin_dequeue_files(f, path);
      }
      fsl_buffer_clear(&canon);
    }
  }else{
    rc = fsl_checkin_dequeue_files(f, NULL);
  }
  return rc;
}

char fsl_checkin_file_is_enqueued(fsl_cx * f, char const * zName,
                                  char relativeToCwd){
  /* TODO: rewrite this to use f->ckin.selectedIds */
  fsl_db * db;
  if(!f || !zName || !*zName) return FSL_RC_MISUSE;
  else if(!(db = fsl_needs_checkout(f))) return FSL_RC_NOT_A_CHECKOUT;
  else if(!f->ckin.selectedIds.entryCount){
    /* Behave like fsl_is_enqueued() SQL function. */
    return 1;
  }
  else {
    char rv = 0;
    fsl_buffer canon = fsl_buffer_empty;
    int rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &canon);
    if(!rc){
      fsl_id_t vfid = 0;
      rc = fsl_filename_to_vfile_id(f, (char const *)canon.mem, &vfid);
      rv = (rc && (vfid>0))
        ? 0
        : ((vfid>0)
           ? fsl_id_bag_contains(&f->ckin.selectedIds, vfid)/*asserts that arg2!=0*/
           : 0);
    }
    fsl_buffer_clear(&canon);
    return rv;
  }
}


void fsl_checkin_discard(fsl_cx * f){
  if(f){
    fsl_id_bag_clear(&f->ckin.selectedIds);
    fsl_deck_finalize(&f->ckin.mf);
  }
}

/**
   Calculates the F-cards for deck d based on the commit file
   selection list and the contents of the vfile table (where vid==the
   vid parameter). vid is the version to check against, and this code
   assumes that the vfile table has been populated with that version.

   If pBaseline is not NULL then d is calculated as being a delta
   from pBaseline, but d->B is not modified by this routine.

   On success, d->F.list will contain "all the F-cards it needs."
   Currently it generates a baseline manifest (with a B-card, which
   isn't all that cool), but it will be changed so that if pBaseline
   is not NULL, only differences are emitted as F-cards.

   If changeCount is not NULL, then on success it is set to the number
   of F-cards added to d due to changes queued via the checkin process
   (as opposed to those added solely for delta inheritance reasons).
*/
static
int fsl_checkin_calc_F_cards2( fsl_cx * f, fsl_deck * d,
                               fsl_deck * pBaseline, fsl_id_t vid,
                               fsl_size_t * changeCount){
  int rc = 0;
  fsl_db * dbR = fsl_needs_repo(f);
  fsl_db * dbC = fsl_needs_checkout(f);
  fsl_stmt stmt = fsl_stmt_empty;
  fsl_stmt * q = &stmt;
  char * fUuid = NULL;
  fsl_card_F const * pFile = NULL;
  fsl_size_t changeCounter = 0;
  if(!f) return FSL_RC_MISUSE;
  else if(!dbR) return FSL_RC_NOT_A_REPO;
  else if(!dbC) return FSL_RC_NOT_A_CHECKOUT;
  assert( (!pBaseline || !pBaseline->B.uuid) && "Baselines must not have a baseline." );
  assert( d->B.baseline ? (!pBaseline || pBaseline==d->B.baseline) : 1 );
  assert(vid>=0);
#define RC if(rc) goto end

  if(pBaseline){
    assert(!d->B.baseline);
    assert(0!=vid);
    rc = fsl_deck_F_rewind(pBaseline);
    RC;
    fsl_deck_F_next( pBaseline, &pFile );
  }

  rc = fsl_vfile_changes_scan(f, vid, FSL_VFILE_CKSIG_NONE);
  RC;

  rc = fsl_db_prepare( dbC, q,
                       "SELECT "
                       /*0*/"fsl_is_enqueued(vf.id) as isSel, "
                       /*1*/"vf.id,"
                       /*2*/"vf.vid,"
                       /*3*/"vf.chnged,"
                       /*4*/"vf.deleted,"
                       /*5*/"vf.isexe,"
                       /*6*/"vf.islink,"
                       /*7*/"vf.rid,"
                       /*8*/"mrid,"
                       /*9*/"pathname,"
                       /*10*/"origname, "
                       /*11*/"b.rid, "
                       /*12*/"b.uuid "
                       "FROM vfile vf LEFT JOIN blob b ON vf.mrid=b.rid "
                       "WHERE"
                       " vf.vid=%"FSL_ID_T_PFMT" AND"
#if 0
                       /* Historical (fossil(1)). This introduces an interesting
                          corner case which i would like to avoid here because
                          it causes a "no files changed" error in the checkin
                          op. The behaviour is actually correct (and the deletion
                          is picked up) but fsl_checkin_commit() has no mechanism
                          for catching this particular case. So we'll try a
                          slightly different approach...
                       */
                       " (NOT deleted OR NOT isSel)"
#else
                       " ((NOT deleted OR NOT isSel)"
                       "  OR (deleted AND isSel))" /* workaround to allow
                                                     us to count deletions via
                                                     changeCounter. */
#endif
                       " ORDER BY fsl_if_enqueued(vf.id, pathname, origname)",
                       (fsl_id_t)vid);
  RC;
  /* MARKER(("SQL:\n%s\n", (char const *)q->sql.mem)); */
  while( FSL_RC_STEP_ROW==fsl_stmt_step(q) ){
    int const isSel = fsl_stmt_g_int32(q,0);
#if 0
    fsl_id_t const vfid = fsl_stmt_g_id(q,1);
    fsl_id_t const vid = fsl_stmt_g_id(q,2);
#endif
    int const changed = fsl_stmt_g_int32(q,3);
    int const deleted = fsl_stmt_g_int32(q,4);
    int const isExe = fsl_stmt_g_int32(q,5);
    int const isLink = fsl_stmt_g_int32(q,6);
    fsl_id_t const rid = fsl_stmt_g_id(q,7);
    fsl_id_t const mergeRid = fsl_stmt_g_id(q,8);
    char const * zName = fsl_stmt_g_text(q, 9, NULL);
    char const * zOrig = fsl_stmt_g_text(q, 10, NULL);
    fsl_id_t const frid = fsl_stmt_g_id(q,11);
    char const * zUuid = fsl_stmt_g_text(q, 12, NULL);
    fsl_file_perm_t perm = FSL_FILE_PERM_REGULAR;
    int cmp;
    fsl_id_t fileBlobRid = rid;
    int const renamed = (zOrig && *zOrig) ? fsl_strcmp(zName,zOrig) : 0
      /* For some as-yet-unknown reason, some fossil(1) code
         sets (origname=pathname WHERE origname=NULL). e.g.
         the 'mv' command does that.
      */;
    if(zOrig && !renamed) zOrig = NULL;
    fUuid = NULL;

    if(!isSel && !zUuid){
      assert(!rid);
      assert(!mergeRid);
      /* An unselected ADDed file. Skip it. */
      continue;
    }

    if(isExe) perm = FSL_FILE_PERM_EXE;
    else if(isLink){
      fsl_fatal(FSL_RC_NYI, "This code does not yet deal "
                "with symlinks. file: %s", zName)
        /* does not return */;
      perm = FSL_FILE_PERM_LINK;
    }
    /*
      TODO: symlinks
    */

    if(!f->cache.markPrivate){
      rc = fsl_content_make_public(f, frid);
      if(rc) break;
    }

#if 0
    if(mergeRid && (mergeRid != rid)){
      fsl_fatal(FSL_RC_NYI, "This code does not yet deal "
                "with merges. file: %s", zName)
        /* does not return */;
    }
#endif
    while(pFile && fsl_strcmp(pFile->name, zName)<0){
      /* Baseline has files with lexically smaller names.  This
         indicates files deleted somewhere between baseline manifest
         and the delta. Depending on which query we use, it can also
         pickup deleted/selected files. The problem with that is this
         interesting corner case:

         f-rm th1ish/makefile.gnu
         f-checkin ... th1ish/makefile.gnu

         makefile.gnu does not get picked up by the historical query
         but gets picked up here. We really need to ++changeCounter in
         that case, but we don't know we're in that case because we're
         now traversing a filename which is not in the result set.
         The end result (because we don't increment changeCounter) is
         that fsl_checkin_commit() thinks we have no made any changes
         and errors out. If we ++changeCounter for all deletions we
         have a different corner case, where a no-change commit is not
         seen as such because we've counted deletions from (other)
         versions between the baseline and the checkout.

      */
#if 0
      /* only need this if we use the historical query */
      if(fsl_checkin_file_is_enqueued(f, pFile->name, 0)){
        /* Workaround for the above corner-case. Why is it that
           Richard's original fossil(1) algos never need such
           workarounds? Something to ponder.

           LOL. And now we need to change the query anyway to pick up
           deleted/selected entries (just for counting purposes!)
           when generating a baseline (which never hits this block).

           Another potential corner case:

           f-rm file1 file3
           f-checkin ...

           by default all files are selected. Let's assume file2 was
           deleted somewhere between the baseline and
           checkout. is-enqueued will say "true" because an empy
           checkin list is the same as a full checkin list. So we'll
           count them as changes here even though we should not.  To
           work around that we have to know if vfile contains
           pFile->name. And then there are probably other
           renaming-related corner cases.
        */
        fsl_id_t idCheck = 0;
        fsl_checkout_filename_vfile_id(f, pFile->name, 0, &idCheck);
        if(idCheck>0){
          ++changeCounter;
          MARKER(("Deletion change-counter kludge: %s\n", pFile->name));
        }
      }
#endif
      rc = fsl_deck_F_add(d, pFile->name, NULL, pFile->perm, NULL);
      if(rc) break;
      fsl_deck_F_next(pBaseline, &pFile);
    }

    if(isSel && (changed || deleted || renamed)){
      /* MARKER(("isSel && (changed||deleted||renamed): %s\n", zName)); */
      ++changeCounter;
      if(deleted){
        zOrig = NULL;
      }else if(changed){
        rc = fsl_checkin_import_file(f, zName, rid, &fileBlobRid, &fUuid);
        RC;
        /* MARKER(("New content: %d / %s / %s\n", (int)fileBlobRid, fUuid, zName)); */
        if(0 != fsl_uuidcmp(zUuid, fUuid)){
          zUuid = fUuid;
        }
      }else{
        assert(renamed);
        assert(zOrig);
      }
    }
    assert(!rc);
    cmp = 1;
    if(!pFile
       || (cmp = fsl_strcmp(pFile->name,zName))!=0
       /* ^^^^ the cmp assignment must come right after (!pFile)! */
       || deleted
       || (perm != pFile->perm)/* permissions change */
       || fsl_strcmp(pFile->uuid, zUuid)!=0
       /* ^^^^^ file changed somewhere between baseline and delta */
       ){
      if(isSel && deleted){
        if(pBaseline /* d is-a delta */){
          /* Deltas mark deletions with F-cards having only 
             a file name (no UUID or permission).
          */
          rc = fsl_deck_F_add(d, zName, NULL, perm, NULL);
        }/*else elide F-card to mark a deletion in a baseline.*/
      }else{
        if(zOrig && !isSel){
          /* File is renamed in vfile but is not being committed, so
             make sure we use the original name for the F-card.
          */
          zName = zOrig;
          zOrig = NULL;
        }
        assert(zUuid);
        assert(fileBlobRid);
        if( !zOrig || !renamed ){
          rc = fsl_deck_F_add(d, zName, zUuid, perm, NULL);
        }else{
          /* Rename this file */
          rc = fsl_deck_F_add(d, zName, zUuid, perm, zOrig);
        }
      }
    }
    fsl_free(fUuid);
    fUuid = NULL;
    RC;
    if( 0 == cmp ){
      fsl_deck_F_next(pBaseline, &pFile);
    }
  }/*while step()*/

  while( !rc && pFile ){
    /* Baseline has remaining files with lexically larger names. Let's import them. */
    rc = fsl_deck_F_add(d, pFile->name, NULL, pFile->perm, NULL);
    if(!rc) fsl_deck_F_next(pBaseline, &pFile);
  }

  end:
#undef RC
  fsl_free(fUuid);
  fsl_stmt_finalize(q);
  if(!rc && changeCount) *changeCount = changeCounter;
  return rc;
}

/**
   Cancels all symbolic tags (branches) on the given version by
   adding one T-card to d for each active branch tag set on vid.
   When creating a branch, d would represent the branch and vid
   would be the version being branched from.

   Returns 0 on success.
*/
static int fsl_cancel_sym_tags( fsl_deck * d, fsl_id_t vid ){
  int rc;
  fsl_stmt q = fsl_stmt_empty;
  fsl_db * db = fsl_needs_repo(d->f);
  assert(db);
  rc = fsl_db_prepare(db, &q,
                      "SELECT tagname FROM tagxref, tag"
                      " WHERE tagxref.rid=%"FSL_ID_T_PFMT
                      " AND tagxref.tagid=tag.tagid"
                      "   AND tagtype>0 AND tagname GLOB 'sym-*'"
                      " ORDER BY tagname",
                      (fsl_id_t)vid);
  while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){
    const char *zTag = fsl_stmt_g_text(&q, 0, NULL);
    rc = fsl_deck_T_add(d, FSL_TAGTYPE_CANCEL,
                        NULL, zTag, "Cancelled by branch.");
  }
  fsl_stmt_finalize(&q);
  return rc;
}

#if 0
static int fsl_leaf_set( fsl_cx * f, fsl_id_t rid, char isLeaf ){
  int rc;
  fsl_stmt * st = NULL;
  fsl_db * db = fsl_needs_repo(f);
  assert(db);
  rc = fsl_db_prepare_cached(db, &st, isLeaf
                             ? "INSERT OR IGNORE INTO leaf(rid) VALUES(?)"
                             : "DELETE FROM leaf WHERE rid=?");
  if(!rc){
    fsl_stmt_bind_id(st, 1, rid);
    fsl_stmt_step(st);
    fsl_stmt_cached_yield(st);
  }
  if(rc){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}
#endif

/**
   Checks vfile for any files (where chnged in (2,3,4,5)), i.e.
   having something to do with a merge. If either all of those
   changes are enqueued for checkin, or none of them are, then
   this function returns 0, otherwise it sets f's error
   state and returns non-0.
*/
static int fsl_checkin_check_for_partial_merge(fsl_cx * f){
  if(!f->ckin.selectedIds.entryCount){
    /* All files are considered enqueued. */
    return 0;
  }else{
    fsl_db * db = fsl_cx_db_checkout(f);
    fsl_int32_t counter = 0;
    int rc =
      fsl_db_get_int32(db, &counter,
                       "SELECT COUNT(*) FROM ("
#if 1
                       "SELECT DISTINCT fsl_is_enqueued(id)"
                       " FROM vfile WHERE chnged IN (2,3,4,5)"
#else
                       "SELECT fsl_is_enqueued(id) isSel "
                       "FROM vfile WHERE chnged IN (2,3,4,5) "
                       "GROUP BY isSel"
#endif
                       ")"
                       );
    /**
       Result is 0 if no merged files are in vfile, 1 row if isSel is
       the same for all merge-modified files, and 2 if there is a mix
       of selected/unselected merge-modified files.
     */
    if(!rc && (counter>1)){
      assert(2==counter);
      rc = fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Cannot commit only part of a merge. "
                          "Commit either all none of it.");
    }
    return rc;
  }
}

/**
   Populates d with the contents for a FSL_CATYPE_CHECKIN manifest
   based on repository version basedOnVid.

   d is the deck to populate.

   basedOnVid must currently be f->ckout.rid OR the vfile table must
   be current for basedOnVid (see fsl_vfile_changes_scan() and
   fsl_vfile_load_from_rid()). It "should" work with basedOnVid==0 but
   that's untested so far.

   opt is the options object passed to fsl_checkin_commit().
*/
static
int fsl_checkin_calc_manifest( fsl_cx * f, fsl_deck * d,
                               fsl_id_t basedOnVid,
                               fsl_checkin_opt const * opt ){
  int rc;
  fsl_db * dbR = fsl_cx_db_repo(f);
  fsl_db * dbC = fsl_cx_db_checkout(f);
  fsl_stmt q = fsl_stmt_empty;
  fsl_deck dBase = fsl_deck_empty;
  char const * zColor;
  assert(d->f == f);
  assert(FSL_CATYPE_CHECKIN==d->type);
#define RC if(rc) goto end

  /* assert(basedOnVid>0); */

  rc = (opt->message && *opt->message)
    ? fsl_deck_C_set( d, opt->message, -1 )
    : fsl_cx_err_set(f, FSL_RC_RANGE,
                     "Cowardly refusing to commit with "
                     "empty checkin comment.");
  RC;

  {
    char const * zUser = opt->user ? opt->user : fsl_cx_user_get(f);
    rc = (zUser && *zUser)
      ? fsl_deck_U_set( d, zUser, -1 )
      : fsl_cx_err_set(f, FSL_RC_RANGE,
                       "Cowardly refusing to commit without "
                       "a user name.");
    RC;
  }

  rc = fsl_checkin_check_for_partial_merge(f);
  RC;

  rc = fsl_deck_D_set( d, (opt->julianTime>0)
                       ? opt->julianTime
                       : fsl_db_julian_now(dbR) );
  RC;

  if(opt->messageMimeType && *opt->messageMimeType){
    rc = fsl_deck_N_set( d, opt->messageMimeType, -1 );
    RC;
  }


  { /* F-cards */
    static char const * errNoFilesMsg =
      "No files have changed. Cowardly refusing to commit.";
    static int const errNoFilesRc = FSL_RC_RANGE;
    fsl_deck * pBase = NULL /* baseline for delta generation purposes */;
    fsl_size_t szD = 0, szB = 0 /* see commentary below */;
    if(basedOnVid && opt->deltaPolicy!=0){
      /* Figure out a baseline for a delta manifest... */
      rc = fsl_deck_load_rid(f, &dBase, basedOnVid, FSL_CATYPE_CHECKIN);
      RC;
      if(dBase.B.uuid){
        /* dBase is a delta. Let's use its baseline for manifest
           generation.
        */
        fsl_id_t const baseRid = fsl_uuid_to_rid(f, dBase.B.uuid);
        fsl_deck_finalize(&dBase);
        assert(baseRid>0);
        rc = fsl_deck_load_rid(f, &dBase, baseRid, FSL_CATYPE_CHECKIN);
        RC;
      }else{
        /* dBase version is a suitable baseline. */
      }
      pBase = &dBase;
      /* MARKER(("Baseline = %d / %s\n", (int)pBase->rid, pBase->uuid)); */
      rc = fsl_deck_B_set(d, pBase->uuid);
      RC;
    }
    rc = fsl_checkin_calc_F_cards2(f, d, pBase, basedOnVid, &szD);
    /* MARKER(("szD=%d\n", (int)szD)); */
    RC;
    if(basedOnVid && !szD){
      rc = fsl_cx_err_set(f, errNoFilesRc, errNoFilesMsg);
      goto end;
    }
    szB = pBase ? pBase->F.list.used : 0;
    /* The following text was copied verbatim from fossil(1). It does
       not apply 100% here (because we use a slightly different
       manifest generation approach) but it clearly describes what's
       going on after the comment block....
    */
    /*
    ** At this point, two manifests have been constructed, either of
    ** which would work for this checkin.  The first manifest (held
    ** in the "manifest" variable) is a baseline manifest and the second
    ** (held in variable named "delta") is a delta manifest.  The
    ** question now is: which manifest should we use?
    **
    ** Let B be the number of F-cards in the baseline manifest and
    ** let D be the number of F-cards in the delta manifest, plus one for
    ** the B-card.  (B is held in the szB variable and D is held in the
    ** szD variable.)  Assume that all delta manifests adds X new F-cards.
    ** Then to minimize the total number of F- and B-cards in the repository,
    ** we should use the delta manifest if and only if:
    **
    **      D*D < B*X - X*X
    **
    ** X is an unknown here, but for most repositories, we will not be
    ** far wrong if we assume X=3.
    */
    ++szD /* account for the d->B card */;
    if(pBase){
      /* For this calculation, i believe the correct approach is to
         simply count the F-cards, including those changed between the
         baseline and the delta, as opposed to only those changed in
         the delta itself.
      */
      szD = 1 + d->F.list.used;
    }
    /* MARKER(("szB=%d szD=%d\n", (int)szB, (int)szD)); */
    if(pBase && (opt->deltaPolicy<0/*non-force-mode*/
                 && !(((int)(szD*szD)) < (((int)szB*3)-9))
                 /* ^^^ see comments above */
                 )
       ){
      /* Too small of a delta to be worth it. Re-calculate
         F-cards with no baseline.

         Maintenance reminder: i initially wanted to update vfile's
         status incrementally as F-cards are calculated, but this
         discard/retry breaks on the retry because vfile's state has
         been modified. Thus instead of updating vfile incrementally,
         we re-scan it after the checkin completes.
      */
      fsl_deck tmp = fsl_deck_empty;
      /* Free up d->F using a kludge... */
      tmp.F = d->F;
      d->F = fsl_deck_empty.F;
      fsl_deck_finalize(&tmp);
      fsl_deck_B_set(d, NULL);
      /* MARKER(("Delta is too big - re-calculating F-cards for a baseline.\n")); */
      szD = 0;
      rc = fsl_checkin_calc_F_cards2(f, d, NULL, basedOnVid, &szD);
      RC;
      if(basedOnVid && !szD){
        rc = fsl_cx_err_set(f, errNoFilesRc, errNoFilesMsg);
        goto end;
      }
    }
  }/* F-cards */

    
  /* parents... */
  if( basedOnVid ){
    char * zParentUuid = fsl_rid_to_artifact_uuid(f, basedOnVid, FSL_CATYPE_CHECKIN);
    if(!zParentUuid){
      assert(f->error.code);
      rc = f->error.code
        ? f->error.code
        : fsl_cx_err_set(f, FSL_RC_NOT_FOUND,
                         "Could not find checkin UUID "
                         "for RID %"FSL_ID_T_PFMT".",
                         (fsl_id_t)basedOnVid);
      goto end;
    }
    rc = fsl_deck_P_add(d, zParentUuid)
      /* pedantic side-note: we could alternately transfer ownership
         of zParentUuid by fsl_list_append()ing it to d->P, but that
         would bypass e.g. any checking that routine chooses to apply.
      */;
    fsl_free(zParentUuid);
    /* if(!rc) rc = fsl_leaf_set(f, basedOnVid, 0); */
    /* TODO:
       if( p->verifyDate ) checkin_verify_younger(vid, zParentUuid, zDate); */
    RC;
    rc = fsl_db_prepare(dbC, &q, "SELECT id, merge FROM vmerge WHERE id=0 OR id<-2");
    RC;
    while( FSL_RC_STEP_ROW == fsl_stmt_step(&q) ){
      char *zMergeUuid;
      fsl_id_t const recId = fsl_stmt_g_id(&q, 0);
      fsl_id_t const mid = fsl_stmt_g_id(&q, 1);
      if( (mid == basedOnVid)
          || (!f->cache.markPrivate && fsl_content_is_private(f,mid))){
        continue;
      }
      zMergeUuid = fsl_rid_to_uuid(f, mid)
        /* FIXME? Adjust the query to join on blob and return the UUID? */
        ;
      assert(zMergeUuid);
      rc = fsl_deck_P_add(d, zMergeUuid);
      fsl_free(zMergeUuid);
      if(!rc){
        /* Is this right? */
        fsl_db_exec(dbC, "DELETE FROM vmerge WHERE id=%"FSL_ID_T_PFMT,
                    (fsl_id_t)recId);
      }
      RC;
      /* TODO:
         if( p->verifyDate ) checkin_verify_younger(mid, zMergeUuid, zDate); */
    }
    fsl_stmt_finalize(&q);
  }

  { /* Q-cards... */
    /* UNTESTED! */
    rc = fsl_db_prepare(dbR, &q,
                        "SELECT CASE vmerge.id WHEN -1 THEN '+' ELSE '-' END AS type,"
                        "  blob.uuid, merge"
                        "  FROM vmerge, blob"
                        " WHERE (vmerge.id=-1 OR vmerge.id=-2)"
                        "   AND blob.rid=vmerge.merge"
                        " ORDER BY 1");
    while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){
      fsl_id_t const mid = fsl_stmt_g_id(&q, 2);
      if( mid != basedOnVid ){
        const char *zType = fsl_stmt_g_text(&q, 0, NULL);
        const char *zCherrypickUuid = fsl_stmt_g_text(&q, 1, NULL);
        assert(zType);
        assert(zCherrypickUuid);
        rc = fsl_deck_Q_add( d, *zType, zCherrypickUuid, NULL );
      }
    }
    fsl_stmt_finalize(&q);
    RC;
  }

  zColor = opt->bgColor;
  if(opt->branch && *opt->branch){
    char * sym = fsl_mprintf("sym-%s", opt->branch);
    if(!sym){
      rc = FSL_RC_OOM;
      goto end;
    }
    rc = fsl_deck_T_add( d, FSL_TAGTYPE_PROPAGATING,
                         NULL, sym, NULL );
    fsl_free(sym);
    RC;
    if(opt->bgColor && *opt->bgColor){
      zColor = NULL;
      rc = fsl_deck_T_add( d, FSL_TAGTYPE_PROPAGATING,
                           NULL, "bgcolor", opt->bgColor);
      RC;
    }
    rc = fsl_deck_T_add( d, FSL_TAGTYPE_PROPAGATING,
                         NULL, "branch", opt->branch );
    RC;
    if(basedOnVid){
      rc = fsl_cancel_sym_tags(d, basedOnVid);
    }
  }
  if(zColor && *zColor){
    /* One-shot background color */
    rc = fsl_deck_T_add( d, FSL_TAGTYPE_ADD,
                         NULL, "bgcolor", opt->bgColor);
    RC;
  }

  if(opt->closeBranch){
    rc = fsl_deck_T_add( d, FSL_TAGTYPE_ADD,
                         NULL, "closed",
                         *opt->closeBranch
                         ? opt->closeBranch
                         : NULL);
    RC;
  }

  {
    /*
      UNTESTED!

      Close any INTEGRATE merges if !op->integrate, or type-0 and
      integrate merges if opt->integrate.
    */
    rc = fsl_db_prepare(dbC, &q,
                        "SELECT uuid, merge FROM vmerge JOIN blob ON merge=rid"
                        " WHERE id %s ORDER BY 1",
                        opt->integrate ? "IN(0,-4)" : "=(-4)");
    while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){
      fsl_id_t const rid = fsl_stmt_g_id(&q, 1);
      if( fsl_rid_is_leaf(f, rid)
          && !fsl_db_exists(dbR, /* Is not closed already... */
                            "SELECT 1 FROM tagxref "
                            "WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT
                            " AND tagtype>0",
                            FSL_TAGID_CLOSED, (fsl_id_t)rid)){
        const char *zIntegrateUuid = fsl_stmt_g_text(&q, 0, NULL);
        assert(zIntegrateUuid);
        if(!zIntegrateUuid){
          rc = FSL_RC_OOM;
          goto end;
        }
        rc = fsl_deck_T_add( d, FSL_TAGTYPE_ADD, zIntegrateUuid,
                             "closed", "Closed by integrate-merge." );
      }
    }
    fsl_stmt_finalize(&q);
    RC;
  }


  end:
#undef RC
  fsl_stmt_finalize(&q);
  fsl_deck_finalize(&dBase);
  d->B.baseline = NULL /* if it was set, it was &dBase */;
  if(rc && !f->error.code){
    if(d->error.code){
      fsl_error_move(&d->error, &f->error);
    }else if(dbR->error.code){
      fsl_cx_uplift_db_error(f, dbR);
    }else if(dbC->error.code){
      fsl_cx_uplift_db_error(f, dbC);
    }
  }
  return rc;
}


int fsl_checkin_T_add( fsl_cx * f, fsl_tag_type tagType,
                       fsl_uuid_cstr uuid, char const * name,
                       char const * value){
  return f
    ? fsl_deck_T_add( &f->ckin.mf, tagType, uuid, name, value )
    : FSL_RC_MISUSE;
}


int fsl_checkin_commit(fsl_cx * f, fsl_checkin_opt const * opt,
                       fsl_id_t * newRid, fsl_uuid_str * newUuid ){
  int rc;
  fsl_deck deck = fsl_deck_empty;
  fsl_deck *d = &deck;
  fsl_db * dbC;
  fsl_db * dbR;
  char inTrans = 0;
  char oldPrivate;
  int const oldFlags = f ? f->flags : 0;
  fsl_id_t const vid = f ? f->ckout.rid : 0;
  if(!f || !opt) return FSL_RC_MISUSE;
  else if(!(dbC = fsl_needs_checkout(f))) return FSL_RC_NOT_A_CHECKOUT;
  else if(!(dbR = fsl_needs_repo(f))) return FSL_RC_NOT_A_REPO;
  assert(vid>=0);

  if( fsl_db_exists(dbR, "SELECT 1 FROM tagxref"
                    " WHERE tagid=%d "
                    " AND rid=%"FSL_ID_T_PFMT" AND tagtype>0",
                    FSL_TAGID_CLOSED, (fsl_id_t)vid) ){
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Cannot commit against a closed leaf.");
  }

  fsl_cx_err_reset(f) /* avoid propagating an older error by accident.
                         Did that in test code. */;

  oldPrivate = f->cache.markPrivate;
  if(opt->isPrivate || fsl_content_is_private(f, vid)){
    f->cache.markPrivate = 1;
  }

#define RC if(rc) goto end
  fsl_deck_init(f, d, FSL_CATYPE_CHECKIN);

  rc = fsl_db_transaction_begin(dbR);
  RC;
  inTrans = 1;

  if(f->ckin.mf.T.used){
    /* Transfer accumulated tags. */
    assert(!f->ckin.mf.content.used);
    d->T = f->ckin.mf.T;
    f->ckin.mf.T = fsl_deck_empty.T;
  }
  rc = fsl_checkin_calc_manifest(f, d, vid, opt);
  RC;

  if(opt->calcRCard) f->flags |= FSL_CX_F_CALC_R_CARD;
  else f->flags &= ~FSL_CX_F_CALC_R_CARD;
  rc = fsl_deck_save( d, opt->isPrivate );

  RC;
  assert(d->rid>0);
  assert(d->uuid);
  /* We rescan the vfile, rather than just fiddle with the existing
     one, because our throw-away-and-re-calculate-F-cards bits must
     either do the updating there (which breaks when we re-calculate
     them) or we must somehow know exactly which entries to touch
     here.
  */
  rc = fsl_vfile_changes_scan(f, d->rid,
                              FSL_VFILE_CKSIG_CLEAR_VFILE);
  if(!rc){
    rc = fsl_config_set_id(f, FSL_CONFDB_CKOUT, "checkout", d->rid);
    if(!rc) rc = fsl_cx_update_checkout_uuid(f);
  }
  RC;

  if(opt->dumpManifestFile){
    FILE * out;
    /* MARKER(("Dumping generated manifest to file [%s]:\n", opt->dumpManifestFile)); */
    out = fsl_fopen(opt->dumpManifestFile, "w");
    if(out){
      rc = fsl_deck_output( d, fsl_output_f_FILE, out, &f->error );
      fsl_fclose(out);
    }else{
      rc = fsl_cx_err_set(f, FSL_RC_IO, "Could not open output "
                          "file for writing: %s", opt->dumpManifestFile);
    }
    RC;
  }

  if(d->P.used){
    /* deltify the parent manifest */
    char const * p0 = (char const *)d->P.list[0];
    fsl_id_t const prid = fsl_uuid_to_rid(f, p0);
    /* MARKER(("Deltifying parent manifest #%d...\n", (int)prid)); */
    assert(p0);
    assert(prid>0);
    rc = fsl_content_deltify(f, prid, d->rid, 0);
    RC;
  }

  end:
  f->flags = oldFlags;
#undef RC
  f->cache.markPrivate = oldPrivate;
  /* fsl_buffer_reset(&f->fileContent); */
  if(inTrans){
    if(rc) fsl_db_transaction_rollback(dbR);
    else{
      rc = fsl_db_transaction_commit(dbR);
      if(!rc){
#if 1
        fsl_repo_leaves_rebuild(f)
          /* (Partial!) KLUDGE/workaround for my lack of knowledge about how
             to efficiently update the leaf entries.  Hmmm. Has no
             effect. Hmm. No... it's marking the previous parent as the
             leaf. Hmmm. Better than having them all be leaves, for the time
             being :/. A 'fossil rebuild' fixes this, indicating that our
             metadata is correct, but hinting that part of libfossil's
             crosslinking is missing a piece.
          */;
#endif
        if(newRid) *newRid = d->rid;
        if(newUuid){
          *newUuid = d->uuid;
          d->uuid = NULL /* transfer ownership */;
        }
      }
    }
  }
  if(rc && !f->error.code){
    if(d->error.code) fsl_error_move(&d->error, &f->error);
    else if(dbR->error.code) fsl_cx_uplift_db_error(f, dbR);
  }
  fsl_checkin_discard(f);
  fsl_deck_finalize(d);
  return rc;
}


#undef MARKER