/* -*- 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 #include "fossil-scm/fossil-internal.h" #include "fossil-scm/fossil-checkout.h" #include "fossil-scm/fossil-hash.h" #include /* memcmp() */ /* Only for debugging */ #include #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