Login
fsl_checkout.c at [0ab8c75c49]
Login

File src/fsl_checkout.c artifact 3b2329d308 part of check-in 0ab8c75c49


/* -*- 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 checkout-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)



/**
    Kludge for type-safe strncmp/strnicmp inconsistency.
 */
static int fsl_strnicmp_int(char const *zA, char const * zB, fsl_size_t nByte){
  return fsl_strnicmp( zA, zB, (fsl_int_t)nByte);
}

static int fsl_cx_init_fsscratch(fsl_cx * f){
  enum {
  /* because testing shows a lot of re-allocs via some of the lower-level
     stat()-related bits, we pre-allocate this many bytes into f->fsScratch.
     Curiously, i see almost no difference in (re)allocation behaviour until i
     increase this to over 200.
  */
  InitialScratchCapacity = 512
  };
  int rc = 0;
  if(f->fsScratch.capacity < InitialScratchCapacity){
    rc = fsl_buffer_reserve(&f->fsScratch, InitialScratchCapacity);
    assert(rc || (f->fsScratch.capacity >= InitialScratchCapacity));
  }
  return rc;
}

int fsl_checkout_filename_check( fsl_cx * f, char relativeToCwd,
                                 char const * zOrigName, fsl_buffer * pOut ){
  int rc;
  if(!f || !zOrigName || !*zOrigName) return FSL_RC_MISUSE;
  else if(!f->ckout.dir
          || !fsl_needs_checkout(f)/* will update f's error state*/
          ) return FSL_RC_NOT_A_CHECKOUT;
#if 0
  /* Is this sane? */
  else if(fsl_is_simple_pathname(zOrigName,1)){
    rc = 0;
    if(pOut){
      rc = fsl_buffer_append(pOut, zOrigName, fsl_strlen(zOrigName));
    }
  }
#endif
  else{
    char const * zLocalRoot;
    char const * zFull;
    fsl_size_t nLocalRoot;
    fsl_size_t nFull;
    fsl_buffer * full = &f->fsScratch;
    int (*xCmp)(char const *, char const *,fsl_size_t);
    char endsWithSlash;
    assert((pOut != full) && "Misuse of f->fsScratch!");
    assert(full->used==0 && "Someone did not re-set f->fsScratch OR it is in use higher up the stack.");

    if(!full->mem){
      rc = fsl_cx_init_fsscratch(f);
      if(rc) goto end;
    }

    zLocalRoot = f->ckout.dir;
    assert(zLocalRoot);
    assert(*zLocalRoot);
    nLocalRoot = fsl_strlen(f->ckout.dir);
    assert(nLocalRoot);
    assert('/' == zLocalRoot[nLocalRoot-1]);
    full->used = 0;
    rc = fsl_file_canonical_name2(relativeToCwd ? NULL : zLocalRoot,
                                  zOrigName, full, 1);
#if 0
    MARKER(("canon2: %p (%s) %s ==> %s\n", (void const *)full->mem,
            relativeToCwd ? "cwd" : "ckout", zOrigName, fsl_buffer_cstr(full)));
#endif
    if(rc){
      if(FSL_RC_OOM != rc){
        rc = fsl_cx_err_set(f, rc, "Error #%d (%s) canonicalizing "
                            "file name: %s\n",
                            rc, fsl_rc_cstr(rc),
                            zOrigName);
      }
      goto end;
    }
    zFull = fsl_buffer_cstr2(full, &nFull);
    xCmp = fsl_cx_is_case_sensitive(f)
      ? fsl_strncmp
      : fsl_strnicmp_int;
    assert(zFull);
    assert(nFull>0);
    endsWithSlash = '/' == zFull[nFull-1];
    if( ((nFull==nLocalRoot-1 || (nFull==nLocalRoot && endsWithSlash))
         && xCmp(zLocalRoot, zFull, nFull)==0)
        || (nFull==1 && zFull[0]=='/' && nLocalRoot==1 && zLocalRoot[0]=='/') ){
      enum { OutputDot = 1 };
      /* Special case.  zOrigName refers to zLocalRoot directory.

         Outputing "." instead of nothing is a historical decision
         which may be worth re-evaluating. Currently fsl_cx_stat() relies
         on it.
      */
      if(pOut){
        char const * zOut;
        fsl_size_t nOut;
        if(endsWithSlash){ /* retain trailing slash */
          zOut = "./";
          nOut = 2;
        }else{
          zOut = ".";
          nOut = 1;
        };
        rc = fsl_buffer_append(pOut, zOut, nOut);
      }else{
        rc = 0;
      }
      goto end;
    }

    if( nFull<=nLocalRoot || xCmp(zLocalRoot, zFull, nLocalRoot) ){
      rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                          "File is outside of checkout tree: %s",
                          zOrigName);
      goto end;
    }

#if 0
    /*
      This impl leads to inconsistencies in handling of ./foo/bar vs ./foo/bar/
     */
    if(!fsl_is_simple_pathname(zFull+nLocalRoot, 1)){
      rc = fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Filename component does not resolve to "
                          "a \"simple filename\": %s", zFull+nLocalRoot);
    }else if(pOut){
      rc = fsl_buffer_append(pOut, zFull + nLocalRoot, nFull - nLocalRoot);
    }
#else
    if(pOut){
      rc = fsl_buffer_append(pOut, zFull + nLocalRoot, nFull - nLocalRoot);
    }
#endif
    end:
    full->used = 0;
  }
  return rc;
}


/**
    Returns a fsl_checkout_change_t value for the given
    fsl_vfile_change_t value.

    Why are these not consolidated into one enum?
*/
static fsl_checkout_change_t fsl_vfile_to_ckout(int vChange){
  switch(vChange){
    case FSL_VFILE_CHANGE_NONE: return FSL_CKOUT_CHANGE_NONE;
    case FSL_VFILE_CHANGE_MOD: return FSL_CKOUT_CHANGE_MOD;
    case FSL_VFILE_CHANGE_MERGE_MOD: return FSL_CKOUT_CHANGE_MERGE_MOD;
    case FSL_VFILE_CHANGE_MERGE_ADD: return FSL_CKOUT_CHANGE_MERGE_ADD;
    case FSL_VFILE_CHANGE_INTEGRATE_MOD: return FSL_CKOUT_CHANGE_INTEGRATE_MOD;
    case FSL_VFILE_CHANGE_INTEGRATE_ADD: return FSL_CKOUT_CHANGE_INTEGRATE_ADD;
    default:
      return FSL_CKOUT_CHANGE_NONE;
  }
}

int fsl_checkout_changes_visit( fsl_cx * f, fsl_id_t vid,
                                char doScan,
                                fsl_checkout_changes_f visitor,
                                void * state ){
  int rc;
  fsl_db * db;
  fsl_stmt st = fsl_stmt_empty;
  int count = 0;
  fsl_checkout_change_t change;
  fsl_fstat fstat;
  if(!f || !visitor) return FSL_RC_MISUSE;
  db = fsl_needs_checkout(f);
  if(!db) return FSL_RC_NOT_A_CHECKOUT;
  if(vid<0){
    vid = f->ckout.rid;
    assert(vid>=0);
  }
  if(doScan){
    rc = fsl_vfile_changes_scan(f, vid, 0);
    if(rc) goto end;
  }
  rc = fsl_db_prepare(db, &st,
                      "SELECT chnged, deleted, rid, pathname, origname "
                      "FROM vfile WHERE vid=%"FSL_ID_T_PFMT,
                      (fsl_id_t)vid);
  assert(!rc);
  while( FSL_RC_STEP_ROW == fsl_stmt_step(&st) ){
    int const changed = fsl_stmt_g_int32(&st, 0);
    int const deleted = fsl_stmt_g_int32(&st,1);
    fsl_id_t const vrid = fsl_stmt_g_id(&st,2);
    char const * name;
    char const * oname = NULL;
    name = fsl_stmt_g_text(&st, 3, NULL);
    oname = fsl_stmt_g_text(&st,4,NULL);
    if(oname && (0==fsl_strcmp(name, oname))){
      /* Work around a fossil oddity which sets origname=pathname
         during a 'mv' operation.
      */
      oname = NULL;
    }
    change = FSL_CKOUT_CHANGE_NONE;
    if(deleted){
      change = FSL_CKOUT_CHANGE_REMOVED;
    }else if(0==vrid){
      change = FSL_CKOUT_CHANGE_ADDED;
    }else if(NULL != oname){
      change = FSL_CKOUT_CHANGE_RENAMED;
    }else{
      fstat = fsl_fstat_empty;
      if( fsl_cx_stat(f, 0, name, &fstat ) ){
        change = FSL_CKOUT_CHANGE_MISSING;
        fsl_cx_err_reset(f) /* keep FSL_RC_NOT_FOUND from bubbling
                               up to the client! */;
      }else if(!changed){
        continue;
      }else{
        change = fsl_vfile_to_ckout(changed);
      }
    }
    assert(change);
    ++count;
    rc = visitor(state, change, name, oname);
    if(rc){
      if(!f->error.code && (FSL_RC_OOM!=rc)){
        fsl_cx_err_set(f, rc, "Error %s returned from changes callback.",
                       fsl_rc_cstr(rc));
      }
      break;
    }
  }
  end:
  fsl_stmt_finalize(&st);
  if(rc && db->error.code && !f->error.code){
    fsl_cx_uplift_db_error(f, db);
  }

  return rc;
}

int fsl_checkout_file_add( fsl_cx * f, char relativeToCwd, char const * zFilename ){
  int rc;
  fsl_db * db = fsl_needs_checkout(f);
  fsl_fstat fst = fsl_fstat_empty;
  fsl_buffer fname = fsl_buffer_empty;
  char const * zNorm;
  fsl_id_t const vid = f ? f->ckout.rid : 0;
  if(!f) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_CHECKOUT;
  assert(vid>=0);

  rc = fsl_cx_stat2(f, relativeToCwd, zFilename, &fst, &fname, 0);
  if(rc) goto end;
  zNorm = fsl_buffer_cstr(&fname);
  if( fsl_db_exists(db, "SELECT 1 FROM vfile"
                    " WHERE vid=%"FSL_ID_T_PFMT
                    " AND pathname=%Q %s",
                    (fsl_id_t)vid, zNorm,
                    fsl_cx_filename_collation(f)) ){
    rc = fsl_db_exec(db, "UPDATE vfile SET deleted=0,"
                     " mtime=%"FSL_INT64_T_PFMT
                     " WHERE vid=%"FSL_ID_T_PFMT
                     " AND pathname=%Q %s",
                     (fsl_int64_t)fst.mtime,
                     (fsl_id_t)vid, zNorm,
                     fsl_cx_filename_collation(f));
  }else{
    int const chnged = FSL_VFILE_CHANGE_MOD
      /* fossil(1) sets chnged=0 on 'add'ed vfile records, but then the 'status'
         command updates the field to 1. To avoid down-stream inconsistencies
         (such as the ones which lead me here), we'll go ahead and set it to
         1 here.
       */;
    rc = fsl_db_exec(db,
                     "INSERT INTO "
                     "vfile(vid,chnged,deleted,rid,mrid,pathname,isexe,islink,mtime)"
                     "VALUES(%"FSL_ID_T_PFMT",%d,0,0,0,%Q,%d,%d,%"FSL_INT64_T_PFMT")",
                     (fsl_id_t)vid, chnged, zNorm,
                     (FSL_FSTAT_PERM_EXE==fst.perm) ? 1 : 0,
                     (FSL_FSTAT_TYPE_LINK==fst.type) ? 1 : 0,
                     (fsl_int64_t)fst.mtime
                     );
  }

  end:
  fsl_buffer_clear(&fname);
  if(rc && db->error.code && !f->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

int fsl_checkout_file_rm( fsl_cx * f, char relativeToCwd, char const * zFilename,
                          char dirsRecursive ){
  int rc;
  fsl_db * db = fsl_needs_checkout(f);
  fsl_buffer fname = fsl_buffer_empty;
  /* char const * zNorm; */
  char const * zNorm;
  char const * zCollate;
  fsl_id_t const vid = f ? f->ckout.rid : 0;
  if(!f) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_CHECKOUT;
  assert(vid>=0);

  rc = fsl_checkout_filename_check(f, relativeToCwd, zFilename, &fname);
  if(rc) goto end;
  zNorm = fsl_buffer_cstr(&fname);

  /* MARKER(("fsl_checkout_file_rm(%d, %s) ==> %s\n", relativeToCwd, zFilename, zNorm)); */
  assert(zNorm);

  if(fname.used){
    /* Should this be done by fsl_checkout_filename_check()? */
    /* Trim trailing slashes... */
    char * tailCheck = fsl_buffer_str(&fname) + fname.used - 1;
    for( ; (tailCheck>zNorm) && ('/'==*tailCheck); --tailCheck){
      *tailCheck = 0;
      --fname.used;
    }
  }

  zCollate = fsl_cx_filename_collation(f);

  if(dirsRecursive){
    rc = fsl_db_exec(db,
                     "UPDATE vfile SET deleted=1 "
                     "WHERE vid=%"FSL_ID_T_PFMT
                     " AND NOT deleted"
                     " AND (pathname=%Q %s OR "
                     "     (pathname>'%q/' %s AND pathname<'%q0' %s))",
                     (fsl_id_t)vid,
                     zNorm, zCollate,
                     zNorm, zCollate, zNorm, zCollate);
  }else{
    rc = fsl_db_exec(db,
                     "UPDATE vfile SET deleted=1 "
                     "WHERE vid=%"FSL_ID_T_PFMT
                     " AND NOT deleted"
                     " AND pathname=%Q %s",
                     (fsl_id_t)vid,
                     zNorm, zCollate);
  }
  assert(!rc);
  if(!rc){
    /* Remove ADDed-but-not-yet-committed entries... */
    rc = fsl_db_exec(db,
                     "DELETE FROM vfile WHERE vid=%"FSL_ID_T_PFMT
                     " AND rid=0 AND deleted",
                     (fsl_id_t)vid);
  }
  end:
  fsl_buffer_clear(&fname);
  if(rc && db->error.code && !f->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

int fsl_checkout_changes_scan(fsl_cx * f){
  return f
    ? fsl_vfile_changes_scan(f, -1, FSL_VFILE_CKSIG_CLEAR_VFILE)
    : FSL_RC_MISUSE;
}


/* These commit/checkin bits are purely hypothetical... */
struct fsl_commit_opt {
  char const * user;
  char const * comment;
  fsl_confirmation_f confirmer;
  void * confirmerState;
  fsl_list filenameList;
};
typedef struct fsl_commit_opt fsl_commit_opt;

#define fsl_commit_opt_empty_m { \
  NULL/*user*/, NULL/*comment*/,\
  NULL/*confirmer*/, NULL/*confirmerState*/,  \
  fsl_list_empty_m/*filenameList*/\
}

extern const fsl_commit_opt fsl_commit_opt_empty;

const fsl_commit_opt fsl_commit_opt_empty = fsl_commit_opt_empty_m;

int fsl_commit_pending(fsl_cx * f, fsl_commit_opt const * opt ){
  int rc = FSL_RC_NYI;
  fsl_deck mf = fsl_deck_empty;
  /* fsl_deck dco = fsl_deck_empty; */
  char const * user;
  char inTrans = 0;
  fsl_db * db = f ? fsl_needs_checkout(f) : NULL;
  if(!f || !opt) return FSL_RC_MISUSE;
  else if(!db) return FSL_RC_NOT_A_CHECKOUT;

  assert(fsl_cx_db_repo(f));
  assert(f->ckout.dir);

  fsl_deck_init(f, &mf, FSL_CATYPE_CHECKIN);

  user = (opt->user && *opt->user) ? opt->user : fsl_cx_user_get(f);
  if(!user || !*user){
    return fsl_cx_err_set(f, FSL_RC_MISUSE,
                          "Committing requires a valid user name.");
  }
#define RCHECK if(rc) goto end
  rc = fsl_deck_U_set(&mf, user, -1);
  RCHECK;

  /*
    TODO:
    - start transaction

    - create manifest deck.

    - Figure out how to calculate list of files properly for the manifest.

    - Figure out how to calculate a delta manifest.

    - fsl_checkout_changes_visit() and collect the list of changed files.

    - fsl_content_put() each file. Update the deck as we go.

    - save deck

    - don't forget UNDO support?

    - end transaction

    - profit!
   */

#undef RCHECK
  end:
  if(rc && !f->error.code){
    if(mf.error.code) fsl_cx_err_set_e(f, &mf.error);
    else if(db->error.code) fsl_cx_uplift_db_error(f, db);
  }
  fsl_deck_finalize(&mf);
  if(inTrans){
    if(!rc){
      rc = fsl_db_transaction_commit(db);
      if(rc && !f->error.code){
        fsl_cx_uplift_db_error(f, db);
      }
    }else{
      fsl_db_transaction_rollback(db);
    }
  }
  return rc;
}

int fsl_checkout_filename_vfile_id( fsl_cx * f, char const * fn, fsl_id_t vid,
                                    fsl_id_t * rv ){
  fsl_db * db = fsl_cx_db_checkout(f);
  fsl_id_t fnid = 0;
  fsl_stmt * qSel = NULL;
  int rc;
  assert(f);
  assert(db);
  assert(rv);
  if(!fn || !fsl_is_simple_pathname(fn, 1)){
    return fsl_cx_err_set(f, FSL_RC_RANGE,
                          "Filename is not a \"simple\" path: %s",
                          fn);
  }
  else if(vid<=0) vid = f->ckout.rid;
  assert(vid>0);
  *rv = 0;
  rc = fsl_db_prepare_cached(db, &qSel,
                             "SELECT id FROM vfile "
                             "WHERE vid=? AND pathname=?");
  if(rc){
      fsl_cx_uplift_db_error(f, db);
      return rc;
  }
  rc = fsl_stmt_bind_id(qSel, 1, vid);
  if(!rc) rc = fsl_stmt_bind_text(qSel, 2, fn, -1, 0);
  if(rc){
    fsl_stmt_cached_yield(qSel);
  }else{
    rc = fsl_stmt_step(qSel);
    if( FSL_RC_STEP_ROW == rc ){
      rc = 0;
      fnid = fsl_stmt_g_id(qSel, 0);
      assert(fnid>0);
    }else if(FSL_RC_STEP_DONE == rc){
      rc = 0;
    }
    fsl_stmt_cached_yield(qSel);
  }
  if(!rc){
    *rv = fnid;
  }else if(db->error.code){
    fsl_cx_uplift_db_error(f, db);
  }
  return rc;
}

#undef MARKER