Login
checkin.c at [473fd8718c]
Login

File src/checkin.c artifact 52a3048384 part of check-in 473fd8718c


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
  Copyright 2013-2021 Stephan Beal (https://wanderinghorse.net).

  Derived heavily from previous work:

  Copyright (c) 2013 D. Richard Hipp (https://www.hwaci.com/drh/)

  
  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.
  
  *****************************************************************************
  This file houses the code for checkin-level APIS.
*/
#include <assert.h>

#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-checkout.h"
#include "fossil-scm/fossil-confdb.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;
}

int fsl_checkin_file_is_enqueued(fsl_cx * f, char const * zName,
                                 int 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_fileperm_e 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(rc) goto end;
    else 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);
    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_SATYPE_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;
  int deltaPolicy = opt->deltaPolicy;
  assert(d->f == f);
  assert(FSL_SATYPE_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;

  if(deltaPolicy!=0 && fsl_config_get_int32(f, FSL_CONFDB_REPO, 0,
                                            "forbid-delta-manifests")){
    deltaPolicy = 0;
  }else if(deltaPolicy<0 && 0==f->cache.seenDeltaManifest){
    deltaPolicy = 0;
  }
  {
    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_NOOP;
    fsl_deck * pBase = NULL /* baseline for delta generation purposes */;
    fsl_size_t szD = 0, szB = 0 /* see commentary below */;
    if(basedOnVid && deltaPolicy!=0){
      /* Figure out a baseline for a delta manifest... */
      rc = fsl_deck_load_rid(f, &dBase, basedOnVid, FSL_SATYPE_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_SATYPE_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 && (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_SATYPE_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_tagtype_e 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;
}

/**
   Adds the given rid to the "unsent" db list, Returns 0 on success,
   updates f's error state on error.
*/
static int fsl_checkin_add_unsent(fsl_cx * f, fsl_id_t rid){
  fsl_db * const r = fsl_cx_db_repo(f);
  int rc;
  assert(r);
  rc = fsl_db_exec(r,
                   "INSERT OR IGNORE INTO unsent VALUES(%"FSL_ID_T_PFMT")",
                   (fsl_id_t)rid);
  if(rc){
    fsl_cx_uplift_db_error(f, r);
  }
  return rc;
}

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_SATYPE_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(!d->F.list.used){
    rc = fsl_cx_err_set(f, FSL_RC_NOOP,
                        "Cowardly refusing to generate an empty commit.");
    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;
  assert(d->f == f);
  rc = fsl_checkin_add_unsent(f, d->rid);
  RC;
  /*
    A holdover from fossil(1) which doesn't(?) really apply here:
    fsl_db_exec(dbC, "DELETE FROM vvar WHERE name='ci-comment'");
  */
  /*
    todo(?) from fossil(1) follows. Most of this seems to be what the
    vfile handling does (above).

    db_multi_exec("PRAGMA %s.application_id=252006673;", db_name("repository"));
    db_multi_exec("PRAGMA %s.application_id=252006674;", db_name("localdb"));

    // Update the vfile and vmerge tables
    db_multi_exec(
      "DELETE FROM vfile WHERE (vid!=%d OR deleted) AND is_selected(id);"
      "DELETE FROM vmerge;"
      "UPDATE vfile SET vid=%d;"
      "UPDATE vfile SET rid=mrid, chnged=0, deleted=0, origname=NULL"
      " WHERE is_selected(id);"
      , vid, nvid
    );
    db_lset_int("checkout", nvid);


    // Update the isexe and islink columns of the vfile table
    db_prepare(&q,
      "UPDATE vfile SET isexe=:exec, islink=:link"
      " WHERE vid=:vid AND pathname=:path AND (isexe!=:exec OR islink!=:link)"
    );
    db_bind_int(&q, ":vid", nvid);
    pManifest = manifest_get(nvid, CFTYPE_MANIFEST, 0);
    manifest_file_rewind(pManifest);
    while( (pFile = manifest_file_next(pManifest, 0)) ){
      db_bind_int(&q, ":exec", pFile->zPerm && strstr(pFile->zPerm, "x"));
      db_bind_int(&q, ":link", pFile->zPerm && strstr(pFile->zPerm, "l"));
      db_bind_text(&q, ":path", pFile->zName);
      db_step(&q);
      db_reset(&q);
    }
    db_finalize(&q);
  */

  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 );
      fsl_fclose(out);
      if(rc && d->error.code && !f->error.code){
        fsl_error_move(&d->error, &f->error);
      }
    }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(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