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