/* -*- 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 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt
SPDX-License-Identifier: BSD-2-Clause-FreeBSD
SPDX-FileCopyrightText: 2021 The Libfossil Authors
SPDX-ArtifactOfProjectName: Libfossil
SPDX-FileType: Code
Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*
*****************************************************************************
This file houses the code for checkout-level APIS.
*/
#include <assert.h>
#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-checkout.h"
#include "fossil-scm/fossil-hash.h"
#include <string.h> /* memcmp() */
/* 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, bool relativeToCwd,
char const * zOrigName, fsl_buffer * pOut ){
int rc;
if(!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;
}else{
fsl_buffer_reuse(full);
}
zLocalRoot = f->ckout.dir;
assert(zLocalRoot);
assert(*zLocalRoot);
nLocalRoot = f->ckout.dirLen;
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(pOut){
rc = fsl_buffer_append(pOut, zFull + nLocalRoot, nFull - nLocalRoot);
}
end:
fsl_buffer_reuse(full);
}
return rc;
}
/**
Returns a fsl_checkout_change_e value for the given
fsl_vfile_change_e value.
Why are these not consolidated into one enum?
*/
static fsl_checkout_change_e fsl_vfile_to_ckout(int vChange){
switch((fsl_vfile_change_e)vChange){
#define EE(X) case FSL_VFILE_CHANGE_##X: return FSL_CKOUT_CHANGE_##X
EE(NONE);
EE(MOD);
EE(MERGE_MOD);
EE(MERGE_ADD);
EE(INTEGRATE_MOD);
EE(INTEGRATE_ADD);
#undef EE
default:
assert(!"Unhandled fsl_vfile_change_e value!");
return FSL_CKOUT_CHANGE_NONE;
}
}
int fsl_checkout_changes_visit( fsl_cx * f, fsl_id_t vid,
bool 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_e 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);
}
}
if(!change){
MARKER(("INTERNAL ERROR: unhandled vfile.chnged value %d for file [%s]\n",
changed, name));
continue;
}
++count;
rc = visitor(state, change, name, oname);
if(rc){
if(FSL_RC_BREAK==rc){
rc = 0;
break;
}else 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;
}
static bool fsl_co_is_in_vfile(fsl_cx *f,
char const *zFilename){
return fsl_db_exists(fsl_cx_db_checkout(f),
"SELECT 1 FROM vfile"
" WHERE vid=%"FSL_ID_T_PFMT
" AND pathname=%Q %s",
f->ckout.rid, zFilename,
fsl_cx_filename_collation(f));
}
/**
Internal machinery for fsl_checkout_file_add(). zFilename MUST
be a checkout-relative file which is known to exist. fst MUST
be an object populated by fsl_stat()'ing zFilename. isInVFile
MUST be the result of having passed zFilename to fsl_co_is_in_vfile().
*/
static int fsl_checkout_file_add_impl( fsl_cx * f, char const *zFilename,
fsl_fstat const *fst,
bool isInVFile){
int rc = 0;
fsl_db * const db = fsl_needs_checkout(f);
assert(fsl_is_simple_pathname(zFilename, true));
if( isInVFile ){
rc = fsl_db_exec(db, "UPDATE vfile SET deleted=0,"
" mtime=%"PRIi64
" WHERE vid=%"FSL_ID_T_PFMT
" AND pathname=%Q %s",
(int64_t)fst->mtime,
f->ckout.rid, zFilename,
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,%"PRIi64")",
f->ckout.rid, chnged, zFilename,
(FSL_FSTAT_PERM_EXE==fst->perm) ? 1 : 0,
(FSL_FSTAT_TYPE_LINK==fst->type) ? 1 : 0,
(int64_t)fst->mtime
);
}
if(rc) rc = fsl_cx_uplift_db_error2(f, db, rc);
return rc;
}
/**
Internal state for the recursive file-add process.
*/
struct CoAddState {
fsl_cx * f;
fsl_checkout_file_add_opt * opt;
fsl_buffer * absBuf; // absolute path of file to check
fsl_buffer * coRelBuf; // checkout-relative path of absBuf
fsl_fstat fst; // fsl_stat() state of absBuf's file
};
typedef struct CoAddState CoAddState;
static const CoAddState CoAddState_empty =
{NULL, NULL, NULL, NULL, fsl_fstat_empty_m};
/**
fsl_dircrawl_f() impl for recursively adding files to a
repo. state must be a (CoAddState*)/
*/
static int fsl_dircrawl_f_add(fsl_dircrawl_state const *);
/**
Attempts to add file or directory (recursively) cas->absBuf to the
current repository. isCrawling must be true if this is a
fsl_dircrawl()-invoked call, else false.
*/
static int co_add_one(CoAddState * cas, bool isCrawling){
int rc = 0;
fsl_buffer_reuse(cas->coRelBuf);
rc = fsl_cx_stat2(cas->f, cas->opt->relativeToCwd,
fsl_buffer_cstr(cas->absBuf), &cas->fst,
fsl_buffer_reuse(cas->coRelBuf), false);
if(rc) return rc;
switch(cas->fst.type){
case FSL_FSTAT_TYPE_FILE:{
bool skipped = false;
char const * zCoRel = fsl_buffer_cstr(cas->coRelBuf);
bool const isInVFile = fsl_co_is_in_vfile(cas->f, zCoRel);
if(!isInVFile){
if(fsl_reserved_fn_check(cas->f, zCoRel,-1)){
/* ^^^ we need to use fsl_reserved_fn_check(), instead of
fsl_is_reserved_fn(), so that we will inherit any
new checks which require a context object. If that
check fails, though, it updates cas->f with an error
message which we need to suppress here to avoid it
accidedentally propagating and causing downstream
confusion. */
fsl_cx_err_reset(cas->f);
skipped = true;
}else if(cas->opt->checkIgnoreGlobs){
char const * m =
fsl_cx_glob_matches(cas->f, FSL_GLOBS_IGNORE, zCoRel);
if(m) skipped = true;
}
if(!skipped && cas->opt->callback){
bool yes = false;
rc = cas->opt->callback( zCoRel, cas->opt->callbackState,
&yes );
if(rc) goto end;
else if(!yes) skipped = true;
}
}
if(skipped){
++cas->opt->counts.skipped;
}else{
rc = fsl_checkout_file_add_impl(cas->f, zCoRel, &cas->fst,
isInVFile);
if(!rc){
if(isInVFile) ++cas->opt->counts.updated;
else ++cas->opt->counts.added;
}
}
break;
}
case FSL_FSTAT_TYPE_DIR:
if(!isCrawling){
/* Reminder to self: fsl_dircrawl() copies its first argument
for canonicalizing it, so this is safe even though
cas->absBuf may be reallocated during the recursive
call. We're done with these particular contents of
cas->absBuf at this point. */
rc = fsl_dircrawl(fsl_buffer_cstr(cas->absBuf),
fsl_dircrawl_f_add, cas);
if(rc && !cas->f->error.code){
rc = fsl_cx_err_set(cas->f, rc, "fsl_dircrawl() returned %s.",
fsl_rc_cstr(rc));
}
}else{
assert(!"Cannot happen - caught higher up");
fsl_fatal(FSL_RC_ERROR, "Internal API misuse in/around %s().",
__func__);
}
break;
default:
rc = fsl_cx_err_set(cas->f, FSL_RC_TYPE,
"Unhandled filesystem entry type: "
"fsl_fstat_type_e #%d", cas->fst.type);
break;
}
end:
return rc;
}
static int fsl_dircrawl_f_add(fsl_dircrawl_state const *dst){
if(FSL_FSTAT_TYPE_FILE!=dst->entryType) return 0;
CoAddState * cas = (CoAddState*)dst->callbackState;
int rc = fsl_buffer_appendf(fsl_buffer_reuse(cas->absBuf),
"%s/%s", dst->absoluteDir, dst->entryName);
if(!rc) rc = co_add_one(cas, true);
return rc;
}
int fsl_checkout_file_add( fsl_cx * f, fsl_checkout_file_add_opt * opt_ ){
int rc = 0;
CoAddState cas = CoAddState_empty;
fsl_checkout_file_add_opt opt = *opt_
/*use a copy in case the user manages to modify
opt_ from a callback. */;
if(!f) return FSL_RC_MISUSE;
else if(!fsl_needs_checkout(f)) return FSL_RC_NOT_A_CHECKOUT;
assert(f->ckout.rid>0);
opt_->counts = fsl_checkout_file_add_opt_empty.counts;
cas.absBuf = fsl_buffer_reuse(&f->scratch)
/* Reminder: we can't reuse f->fsScratch here b/c it will be used by
recursive calls we make. */;
cas.coRelBuf = fsl_buffer_reuse(&f->fileContent);
rc = fsl_file_canonical_name(opt.filename, cas.absBuf, false);
if(!rc){
cas.f = f;
cas.opt = &opt;
rc = co_add_one(&cas, false);
opt_->counts = opt.counts;
}
fsl_buffer_reuse(cas.absBuf);
fsl_buffer_reuse(cas.coRelBuf);
return rc;
}
int fsl_checkout_file_rm( fsl_cx * f, bool relativeToCwd, char const * zFilename,
bool dirsRecursive ){
int rc;
fsl_db * db = fsl_needs_checkout(f);
fsl_buffer fname = fsl_buffer_empty;
char const * zNorm;
char const * zCollate;
fsl_id_t const vid = f ? f->ckout.rid : 0;
if(!db) return FSL_RC_NOT_A_CHECKOUT;
if(!zFilename || !*zFilename) return FSL_RC_MISUSE;
else 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);
}
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){
rc = fsl_cx_uplift_db_error2(f, db, rc);
}
return rc;
}
int fsl_checkout_changes_scan(fsl_cx * f){
return fsl_vfile_changes_scan(f, -1, 0);
}
int fsl_reserved_fn_check(fsl_cx *f, const char *zPath,
fsl_int_t nPath){
int rc = 0;
if(nPath<0) nPath = (fsl_int_t)fsl_strlen(zPath);
if(fsl_is_reserved_fn(zPath, nPath)){
rc = fsl_cx_err_set(f, FSL_RC_MISUSE,
"Filename is reserved, not legal "
"for adding to a repository: %.*s",
(int)nPath, zPath);
}
return rc;
}
int fsl_checkout_install_schema(fsl_cx *f, bool dropIfExists){
char const * tNames[] = {
"vvar", "vfile", "vmerge", 0
};
int rc;
fsl_db * const db = fsl_needs_checkout(f);
if(!db) return f->error.code;
if(dropIfExists){
char const * t;
int i;
char const * dbName = fsl_db_role_label(FSL_DBROLE_CHECKOUT);
for(i=0; 0!=(t = tNames[i]); ++i){
rc = fsl_db_exec(db, "DROP TABLE IF EXISTS %s.%s",
dbName, t);
if(rc) break;
}
if(!rc){
rc = fsl_db_exec(db, "DROP TRIGGER IF EXISTS "
"%s.vmerge_ck1",
dbName);
}
}else{
if(fsl_db_table_exists(db, FSL_DBROLE_CHECKOUT,
tNames[0])){
return 0;
}
}
rc = fsl_db_exec_multi(db, "%s", fsl_schema_checkout());
return fsl_cx_uplift_db_error2(f, db, rc);
}
bool fsl_checkout_has_changes(fsl_cx *f){
fsl_db * const db = fsl_cx_db_checkout(f);
if(!db) return false;
return fsl_db_exists(db,
"SELECT 1 FROM vfile WHERE chnged "
"OR coalesce(origname != pathname, 0)")
|| fsl_db_exists(db,"SELECT 1 FROM vmerge");
}
int fsl_checkout_clear_merge_state( fsl_cx *f ){
fsl_db * d = fsl_needs_checkout(f);
int rc;
if(d){
rc = fsl_db_exec(d,"DELETE FROM vmerge");
rc = fsl_cx_uplift_db_error2(f, d, rc);
}else{
rc = FSL_RC_NOT_A_CHECKOUT;
}
return rc;
}
int fsl_checkout_clear_db(fsl_cx *f){
fsl_db * const db = fsl_needs_checkout(f);
if(!db) return f->error.code;
return fsl_db_exec_multi(db,
"DELETE FROM vfile;"
"DELETE FROM vmerge;"
"DELETE FROM vvar WHERE name IN"
"('checkout','checkout-hash')");
}
int fsl_is_locally_modified(fsl_cx * f, const char * zFilename,
fsl_size_t origSize,
const char * zOrigHash,
fsl_int_t zOrigHashLen,
bool * isModified){
int rc = 0;
int const hashLen = zOrigHashLen>=0
? zOrigHashLen : fsl_is_uuid(zOrigHash);
fsl_buffer hash = fsl_buffer_empty;
fsl_buffer * fname = &f->fsScratch;
fsl_fstat fst = fsl_fstat_empty;
if(!fsl_is_uuid_len(hashLen)){
return fsl_cx_err_set(f, FSL_RC_RANGE, "%s(): invalid hash length "
"%d for file: %s", __func__, hashLen, zFilename);
}else if(!f->ckout.dir){
return fsl_cx_err_set(f, FSL_RC_NOT_A_CHECKOUT,
"%s() requires a checkout.", __func__);
}
assert(!fname->used && "Internal misuse of fsl_cx::fsScratch");
if(!fsl_is_absolute_path(zFilename)){
fsl_buffer_reuse(fname);
rc = fsl_file_canonical_name2(f->ckout.dir, zFilename, fname, false);
if(rc) goto end;
zFilename = fsl_buffer_cstr(fname);
}
rc = fsl_stat(zFilename, &fst, false);
if(0==rc){
if(origSize!=fst.size){
*isModified = true;
return 0;
}
}else{
rc = fsl_cx_err_set(f, rc, "%s(): stat() failed for file: %s",
__func__, zFilename);
goto end;
}
if(FSL_STRLEN_SHA1==hashLen){
rc = fsl_sha1sum_filename(zFilename, &hash);
}else if(FSL_STRLEN_K256==hashLen){
rc = fsl_sha3sum_filename(zFilename, &hash);
}else{
assert(!"This \"cannot happen\".");
return FSL_RC_UNSUPPORTED;
}
if(rc){
rc = fsl_cx_err_set(f, rc, "%s: error hashing file: %s",
__func__, zFilename);
}else{
assert(hashLen==(int)hash.used);
*isModified = memcmp(hash.mem, zOrigHash, (size_t)hashLen)
? true : false;
/*MARKER(("%d: %s %s %s\n", *isModified, zOrigHash,
(char const *)hash.mem, zFilename));*/
}
end:
fsl_buffer_reuse(fname);
fsl_buffer_clear(&hash);
return rc;
}
#undef MARKER