/* -*- 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" /* 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, 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_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, 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_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(!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); rc = fsl_reserved_fn_check(f, zNorm, (fsl_int_t)fname.used); if(rc) goto end; 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=%"PRIi64 " WHERE vid=%"FSL_ID_T_PFMT " AND pathname=%Q %s", (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,%"PRIi64")", (fsl_id_t)vid, chnged, zNorm, (FSL_FSTAT_PERM_EXE==fst.perm) ? 1 : 0, (FSL_FSTAT_TYPE_LINK==fst.type) ? 1 : 0, (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 fsl_vfile_changes_scan(f, -1, 0); } /* 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_SATYPE_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); RCHECK; /* TODO: - UNDO support? */ #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; } 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')"); } #undef MARKER