/* -*- 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 checkin-level APIS. */ #include #include "fossil-scm/internal.h" #include "fossil-scm/checkout.h" #include "fossil-scm/confdb.h" /* Only for debugging */ #include #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) /** Expects f to have an opened checkout. Assumes zRelName is a checkout-relative simple path. It loads the file's contents and stores them into the blob table. If rid is not NULL, *rid is assigned the blob.rid (possibly new, possilbly re-used!). If uuid is not NULL then *uuid is assigned to the content's UUID. The *uuid bytes are owned by the caller, who must eventually fsl_free() them. If content with the same UUID already exists, it does not get re-imported but rid/uuid will (if not NULL) contain the old values. If parentRid is >0 then it must refer to the previous version of zRelName's content. The parent version gets deltified vs the new one. Note that deltification is a suggestion which the library will ignore if (e.g.) the parent content is already a delta of something else. The wise caller will have a transaction in place when calling this. Returns 0 on success. On error rid and uuid are not modified. */ static int fsl_checkin_import_file( fsl_cx * const f, char const * const zRelName, fsl_id_t parentRid, bool allowMergeConflict, fsl_id_t * const rid, fsl_uuid_str * const uuid){ fsl_buffer * nbuf = fsl__cx_scratchpad(f); fsl_size_t const oldSize = nbuf->used; fsl_buffer * const fbuf = &f->cache.fileContent; char const * fn; int rc; fsl_id_t fnid = 0; fsl_id_t rcRid = 0; assert(!fbuf->used && "Misuse of f->fileContent"); assert(f->ckout.dir); rc = fsl__repo_filename_fnid2(f, zRelName, &fnid, 1); if(rc) goto end; assert(fnid>0); rc = fsl_buffer_appendf(nbuf, "%s%s", f->ckout.dir, zRelName); nbuf->used = oldSize; if(rc) goto end; fn = fsl_buffer_cstr(nbuf) + oldSize; rc = fsl_buffer_fill_from_filename( fbuf, fn ); if(rc){ fsl_cx_err_set(f, rc, "Error %s importing file: %s", fsl_rc_cstr(rc), fn); goto end; }else if(!allowMergeConflict && fsl_buffer_contains_merge_marker(fbuf)){ rc = fsl_cx_err_set(f, FSL_RC_CONFLICT, "File contains a merge conflict marker: %s", zRelName); goto end; } rc = fsl__content_put( f, fbuf, &rcRid ); if(!rc){ assert(rcRid > 0); if(parentRid>0){ rc = fsl__content_deltify(f, parentRid, rcRid, 0); } if(!rc){ if(rid) *rid = rcRid; if(uuid){ *uuid = fsl_rid_to_uuid(f, rcRid); if(!*uuid) rc = (f->error.code ? f->error.code : FSL_RC_OOM); } } } end: fsl__cx_scratchpad_yield(f, nbuf); fsl__cx_content_buffer_yield(f); assert(0==fbuf->used); return rc; } int fsl_filename_to_vfile_ids( fsl_cx * f, fsl_id_t vid, fsl_id_bag * dest, char const * zName, bool changedOnly){ fsl_stmt st = fsl_stmt_empty; fsl_db * const db = fsl_needs_ckout(f); int rc; fsl_buffer * sql = 0; if(!db) return FSL_RC_NOT_A_CKOUT; sql = fsl__cx_scratchpad(f); if(0>=vid) vid = f->ckout.rid; if(zName && *zName && !('.'==*zName && !zName[1])){ rc = fsl_buffer_appendf(sql, "SELECT id FROM vfile WHERE vid=%" FSL_ID_T_PFMT " AND fsl_match_vfile_or_dir(pathname,%Q)", vid, zName); }else{ rc = fsl_buffer_appendf(sql, "SELECT id FROM vfile WHERE vid=%" FSL_ID_T_PFMT, vid); } if(rc) goto end; else if(changedOnly){ rc = fsl_buffer_append(sql, " AND (chnged OR deleted OR rid=0 " "OR (origname IS NOT NULL AND " " origname<>pathname))", -1); if(rc) goto end; } rc = fsl_buffer_appendf(sql, " /* %s() */", __func__); if(rc) goto end; rc = fsl_db_prepare(db, &st, "%b", sql); while(!rc && (FSL_RC_STEP_ROW == (rc=fsl_stmt_step(&st)))){ rc = fsl_id_bag_insert( dest, fsl_stmt_g_id(&st, 0) ); } if(FSL_RC_STEP_DONE==rc) rc = 0; end: fsl__cx_scratchpad_yield(f, sql); fsl_stmt_finalize(&st); if(rc && !f->error.code && db->error.code){ fsl_cx_uplift_db_error(f, db); } return rc; } int fsl_filename_to_vfile_id( fsl_cx * f, fsl_id_t vid, char const * zName, fsl_id_t * vfid ){ fsl_db * db = fsl_needs_ckout(f); int rc; fsl_stmt st = fsl_stmt_empty; assert(db); if(!db) return FSL_RC_NOT_A_CKOUT; else if(!zName || !fsl_is_simple_pathname(zName, true)){ return fsl_cx_err_set(f, FSL_RC_RANGE, "Filename is not a \"simple\" path: %s", zName); } if(0>=vid) vid = f->ckout.rid; rc = fsl_db_prepare(db, &st, "SELECT id FROM vfile WHERE vid=%" FSL_ID_T_PFMT " AND pathname=%Q %s /*%s()*/", vid, zName, fsl_cx_filename_collation(f), __func__); if(!rc){ rc = fsl_stmt_step(&st); switch(rc){ case FSL_RC_STEP_ROW: rc = 0; *vfid = fsl_stmt_g_id(&st, 0); break; case FSL_RC_STEP_DONE: rc = 0; /* fall through */ default: *vfid = 0; } fsl_stmt_finalize(&st); } if(rc){ rc = fsl_cx_uplift_db_error2(f, db, rc); } return rc; } /** Internal helper for fsl_checkin_enqueue() and fsl_checkin_dequeue(). Prepares, if needed, st with a query to fetch a vfile entry where vfile.id=vfid, then passes that name on to opt->callback(). Returns 0 on success. */ static int fsl_xqueue_callback(fsl_cx * f, fsl_db * db, fsl_stmt * st, fsl_id_t vfid, fsl_checkin_queue_opt const * opt){ int rc; assert(opt->callback); if(!st->stmt){ rc = fsl_db_prepare(db, st, "SELECT pathname FROM vfile " "WHERE id=?1"); if(rc) return fsl_cx_uplift_db_error2(f, db, rc); } fsl_stmt_bind_id(st, 1, vfid); rc = fsl_stmt_step(st); switch(rc){ case FSL_RC_STEP_ROW:{ char const * zName = fsl_stmt_g_text(st, 0, NULL); rc = opt->callback(zName, opt->callbackState); break; } case FSL_RC_STEP_DONE: rc = fsl_cx_err_set(f, rc, "Very unexpectedly did not find " "vfile.id which we just found."); break; default: rc = fsl_cx_uplift_db_error2(f, db, rc); break; } fsl_stmt_reset(st); return rc; } int fsl_checkin_enqueue(fsl_cx * f, fsl_checkin_queue_opt const * opt){ fsl_db * const db = fsl_needs_ckout(f); if(!db) return FSL_RC_NOT_A_CKOUT; fsl_buffer * const canon = opt->vfileIds ? 0 : fsl__cx_scratchpad(f); fsl_stmt qName = fsl_stmt_empty; fsl_id_bag _vfileIds = fsl_id_bag_empty; fsl_id_bag const * const vfileIds = opt->vfileIds ? opt->vfileIds : &_vfileIds; int rc = fsl_db_transaction_begin(db); if(rc) return fsl_cx_uplift_db_error2(f, db, rc); if(opt->vfileIds){ if(!fsl_id_bag_count(opt->vfileIds)){ rc = fsl_cx_err_set(f, FSL_RC_MISUSE, "fsl_checkin_queue_opt::vfileIds " "may not be empty."); goto end; } }else{ rc = fsl_ckout_filename_check(f, opt->relativeToCwd, opt->filename, canon); if(rc) goto end; fsl_buffer_strip_slashes(canon); } if(opt->scanForChanges){ rc = fsl_vfile_changes_scan(f, -1, 0); if(rc) goto end; } if(opt->vfileIds){ assert(vfileIds == opt->vfileIds); }else{ assert(vfileIds == &_vfileIds); rc = fsl_filename_to_vfile_ids(f, 0, &_vfileIds, fsl_buffer_cstr(canon), opt->onlyModifiedFiles); } if(rc) goto end; /* Walk through each found ID and queue up any which are not already enqueued. */ for(fsl_id_t vfid = fsl_id_bag_first(vfileIds); !rc && vfid; vfid = fsl_id_bag_next(vfileIds, vfid)){ fsl_size_t const entryCount = f->ckin.selectedIds.entryCount; rc = fsl_id_bag_insert(&f->ckin.selectedIds, vfid); if(!rc && entryCount < f->ckin.selectedIds.entryCount /* Was enqueued */ && opt->callback){ rc = fsl_xqueue_callback(f, db, &qName, vfid, opt); } } end: if(opt->vfileIds){ assert(!canon); assert(!_vfileIds.list); }else{ assert(canon); fsl__cx_scratchpad_yield(f, canon); fsl_id_bag_clear(&_vfileIds); } fsl_stmt_finalize(&qName); if(rc) fsl_db_transaction_rollback(db); else{ rc = fsl_cx_uplift_db_error2(f, db, fsl_db_transaction_commit(db)); } return rc; } int fsl_checkin_dequeue(fsl_cx * const f, fsl_checkin_queue_opt const * opt){ fsl_db * const db = fsl_needs_ckout(f); if(!db) return FSL_RC_NOT_A_CKOUT; int rc = fsl_db_transaction_begin(db); if(rc) return fsl_cx_uplift_db_error2(f, db, rc); fsl_id_bag list = fsl_id_bag_empty; fsl_buffer * canon = 0; char const * fn; fsl_stmt qName = fsl_stmt_empty; if(opt->filename && *opt->filename){ canon = fsl__cx_scratchpad(f); rc = fsl_ckout_filename_check(f, opt->relativeToCwd, opt->filename, canon); if(rc) goto end; else fsl_buffer_strip_slashes(canon); } fn = canon ? fsl_buffer_cstr(canon) : opt->filename; rc = fsl_filename_to_vfile_ids(f, 0, &list, fn, false); if(!rc && list.entryCount){ /* Walk through each found ID and dequeue up any which are enqueued. */ for( fsl_id_t nid = fsl_id_bag_first(&list); !rc && nid; nid = fsl_id_bag_next(&list, nid)){ if(fsl_id_bag_remove(&f->ckin.selectedIds, nid) && opt->callback){ rc = fsl_xqueue_callback(f, db, &qName, nid, opt); } } } end: if(canon) fsl__cx_scratchpad_yield(f, canon); fsl_stmt_finalize(&qName); fsl_id_bag_clear(&list); if(rc) fsl_db_transaction_rollback(db); else{ rc = fsl_cx_uplift_db_error2(f, db, fsl_db_transaction_commit(db)); } return rc; } bool fsl_checkin_is_enqueued(fsl_cx * const f, char const * zName, bool relativeToCwd){ if(!zName || !*zName) return false; else if(!fsl_cx_db_ckout(f)) return false; else if(!f->ckin.selectedIds.entryCount){ /* Behave like fsl_is_enqueued() SQL function. */ return true; } else { bool rv = false; fsl_buffer * const canon = fsl__cx_scratchpad(f); int rc = fsl_ckout_filename_check(f, relativeToCwd, zName, canon); if(!rc){ fsl_id_t vfid = 0; rc = fsl_filename_to_vfile_id(f, 0, fsl_buffer_cstr(canon), &vfid); rv = (rc && (vfid>0)) ? false : ((vfid>0) ? fsl_id_bag_contains(&f->ckin.selectedIds, vfid) /* ^^^^ asserts that arg2!=0*/ : false); } fsl__cx_scratchpad_yield(f, canon); return rv; } } void fsl_checkin_discard(fsl_cx * const f){ if(f){ fsl_id_bag_clear(&f->ckin.selectedIds); fsl_deck_finalize(&f->ckin.mf); } } /** Adds the given rid to the "unsent" db list, Returns 0 on success, updates f's error state on error. */ static int fsl_checkin_add_unsent(fsl_cx * f, fsl_id_t rid){ fsl_db * const r = fsl_cx_db_repo(f); int rc; assert(r); rc = fsl_db_exec(r,"INSERT OR IGNORE INTO unsent " "VALUES(%" FSL_ID_T_PFMT ")", rid); if(rc){ fsl_cx_uplift_db_error(f, r); } return rc; } /** Calculates the F-cards for deck d based on the commit file selection list and the contents of the vfile table (where vid==the vid parameter). vid is the version to check against, and this code assumes that the vfile table has been populated with that version and its state represents a recent scan (with no filesystem-level changes made since the scan). If pBaseline is not NULL then d is calculated as being a delta from pBaseline, but d->B is not modified by this routine. On success, d->F.list will contain "all the F-cards it needs." If changeCount is not NULL, then on success it is set to the number of F-cards added to d due to changes queued via the checkin process (as opposed to those added solely for delta inheritance reasons). */ static int fsl_checkin_calc_F_cards2( fsl_cx * f, fsl_deck * d, fsl_deck * pBaseline, fsl_id_t vid, fsl_size_t * changeCount, fsl_checkin_opt const * ciOpt){ int rc = 0; fsl_db * dbR = fsl_needs_repo(f); fsl_db * dbC = fsl_needs_ckout(f); fsl_stmt stUpdateFileRid = fsl_stmt_empty; fsl_stmt stmt = fsl_stmt_empty; fsl_stmt * q = &stmt; char * fUuid = NULL; fsl_card_F const * pFile = NULL; fsl_size_t changeCounter = 0; if(!f) return FSL_RC_MISUSE; else if(!dbR) return FSL_RC_NOT_A_REPO; else if(!dbC) return FSL_RC_NOT_A_CKOUT; assert( (!pBaseline || !pBaseline->B.uuid) && "Baselines must not have a baseline." ); assert( d->B.baseline ? (!pBaseline || pBaseline==d->B.baseline) : 1 ); assert(vid>=0); #define RC if(rc) goto end if(pBaseline){ assert(!d->B.baseline); assert(0!=vid); rc = fsl_deck_F_rewind(pBaseline); RC; fsl_deck_F_next( pBaseline, &pFile ); } rc = fsl_db_prepare(dbC, &stUpdateFileRid, "UPDATE vfile SET mrid=?1, rid=?1, " "mhash=NULL WHERE id=?2"); RC; rc = fsl_db_prepare( dbC, q, "SELECT " /*0*/"fsl_is_enqueued(vf.id) as isSel, " /*1*/"vf.id," /*2*/"vf.vid," /*3*/"vf.chnged," /*4*/"vf.deleted," /*5*/"vf.isexe," /*6*/"vf.islink," /*7*/"vf.rid," /*8*/"mrid," /*9*/"pathname," /*10*/"origname, " /*11*/"b.rid, " /*12*/"b.uuid " "FROM vfile vf LEFT JOIN blob b ON vf.mrid=b.rid " "WHERE" " vf.vid=%"FSL_ID_T_PFMT" AND" #if 0 /* Historical (fossil(1)). This introduces an interesting corner case which i would like to avoid here because it causes a "no files changed" error in the checkin op. The behaviour is actually correct (and the deletion is picked up) but fsl_checkin_commit() has no mechanism for catching this particular case. So we'll try a slightly different approach... */ " (NOT deleted OR NOT isSel)" #else " ((NOT deleted OR NOT isSel)" " OR (deleted AND isSel))" /* workaround to allow us to count deletions via changeCounter. */ #endif " ORDER BY fsl_if_enqueued(vf.id, pathname, origname)", (fsl_id_t)vid); RC; /* MARKER(("SQL:\n%s\n", (char const *)q->sql.mem)); */ while( FSL_RC_STEP_ROW==fsl_stmt_step(q) ){ int const isSel = fsl_stmt_g_int32(q,0); fsl_id_t const id = fsl_stmt_g_id(q,1); #if 0 fsl_id_t const vid = fsl_stmt_g_id(q,2); #endif int const changed = fsl_stmt_g_int32(q,3); int const deleted = fsl_stmt_g_int32(q,4); int const isExe = fsl_stmt_g_int32(q,5); int const isLink = fsl_stmt_g_int32(q,6); fsl_id_t const rid = fsl_stmt_g_id(q,7); fsl_id_t const mergeRid = fsl_stmt_g_id(q,8); char const * zName = fsl_stmt_g_text(q, 9, NULL); char const * zOrig = fsl_stmt_g_text(q, 10, NULL); fsl_id_t const frid = fsl_stmt_g_id(q,11); char const * zUuid = fsl_stmt_g_text(q, 12, NULL); fsl_fileperm_e perm = FSL_FILE_PERM_REGULAR; int cmp; fsl_id_t fileBlobRid = rid; int const renamed = (zOrig && *zOrig) ? fsl_strcmp(zName,zOrig) : 0 /* For some as-yet-unknown reason, some fossil(1) code sets (origname=pathname WHERE origname=NULL). e.g. the 'mv' command does that. */; if(zOrig && !renamed) zOrig = NULL; fUuid = NULL; if(!isSel && !zUuid){ assert(!rid); assert(!mergeRid); /* An unselected ADDed file. Skip it. */ continue; } if(isExe) perm = FSL_FILE_PERM_EXE; else if(isLink){ fsl__fatal(FSL_RC_NYI, "This code does not yet deal " "with symlinks. file: %s", zName) /* does not return */; perm = FSL_FILE_PERM_LINK; } /* TODO: symlinks */ if(!f->cache.markPrivate){ rc = fsl_content_make_public(f, frid); if(rc) break; } #if 0 if(mergeRid && (mergeRid != rid)){ fsl__fatal(FSL_RC_NYI, "This code does not yet deal " "with merges. file: %s", zName) /* does not return */; } #endif while(pFile && fsl_strcmp(pFile->name, zName)<0){ /* Baseline has files with lexically smaller names. Interesting corner case: f-rm th1ish/makefile.gnu f-checkin ... th1ish/makefile.gnu makefile.gnu does not get picked up by the historical query but gets picked up here. We really need to ++changeCounter in that case, but we don't know we're in that case because we're now traversing a filename which is not in the result set. The end result (because we don't increment changeCounter) is that fsl_checkin_commit() thinks we have no made any changes and errors out. If we ++changeCounter for all deletions we have a different corner case, where a no-change commit is not seen as such because we've counted deletions from (other) versions between the baseline and the checkout. */ rc = fsl_deck_F_add(d, pFile->name, NULL, pFile->perm, NULL); if(rc) break; fsl_deck_F_next(pBaseline, &pFile); } if(rc) goto end; else if(isSel && (changed || deleted || renamed)){ /* MARKER(("isSel && (changed||deleted||renamed): %s\n", zName)); */ ++changeCounter; if(deleted){ zOrig = NULL; }else if(changed){ rc = fsl_checkin_import_file(f, zName, rid, ciOpt->allowMergeConflict, &fileBlobRid, &fUuid); if(!rc) rc = fsl_checkin_add_unsent(f, fileBlobRid); RC; /* MARKER(("New content: %d / %s / %s\n", (int)fileBlobRid, fUuid, zName)); */ if(0 != fsl_uuidcmp(zUuid, fUuid)){ zUuid = fUuid; } fsl_stmt_reset(&stUpdateFileRid); fsl_stmt_bind_id(&stUpdateFileRid, 1, fileBlobRid); fsl_stmt_bind_id(&stUpdateFileRid, 2, id); if(FSL_RC_STEP_DONE!=fsl_stmt_step(&stUpdateFileRid)){ rc = fsl_cx_uplift_db_error(f, stUpdateFileRid.db); assert(rc); goto end; } }else{ assert(renamed); assert(zOrig); } } assert(!rc); cmp = 1; if(!pFile || (cmp = fsl_strcmp(pFile->name,zName))!=0 /* ^^^^ the cmp assignment must come right after (!pFile)! */ || deleted || (perm != pFile->perm)/* permissions change */ || fsl_strcmp(pFile->uuid, zUuid)!=0 /* ^^^^^ file changed somewhere between baseline and delta */ ){ if(isSel && deleted){ if(pBaseline /* d is-a delta */){ /* Deltas mark deletions with F-cards having only a file name (no UUID or permission). */ rc = fsl_deck_F_add(d, zName, NULL, perm, NULL); }/*else elide F-card to mark a deletion in a baseline.*/ }else{ if(zOrig && !isSel){ /* File is renamed in vfile but is not being committed, so make sure we use the original name for the F-card. */ zName = zOrig; zOrig = NULL; } assert(zUuid); assert(fileBlobRid); if( !zOrig || !renamed ){ rc = fsl_deck_F_add(d, zName, zUuid, perm, NULL); }else{ /* Rename this file */ rc = fsl_deck_F_add(d, zName, zUuid, perm, zOrig); } } } fsl_free(fUuid); fUuid = NULL; RC; if( 0 == cmp ){ fsl_deck_F_next(pBaseline, &pFile); } }/*while step()*/ while( !rc && pFile ){ /* Baseline has remaining files with lexically larger names. Let's import them. */ rc = fsl_deck_F_add(d, pFile->name, NULL, pFile->perm, NULL); if(!rc) fsl_deck_F_next(pBaseline, &pFile); } end: #undef RC fsl_free(fUuid); fsl_stmt_finalize(q); fsl_stmt_finalize(&stUpdateFileRid); if(!rc && changeCount) *changeCount = changeCounter; return rc; } /** Cancels all symbolic tags (branches) on the given version by adding one T-card to d for each active branch tag set on vid. When creating a branch, d would represent the branch and vid would be the version being branched from. Returns 0 on success. */ static int fsl_cancel_sym_tags( fsl_deck * d, fsl_id_t vid ){ int rc; fsl_stmt q = fsl_stmt_empty; fsl_db * db = fsl_needs_repo(d->f); assert(db); rc = fsl_db_prepare(db, &q, "SELECT tagname FROM tagxref, tag" " WHERE tagxref.rid=%"FSL_ID_T_PFMT " AND tagxref.tagid=tag.tagid" " AND tagtype>0 AND tagname GLOB 'sym-*'" " ORDER BY tagname", (fsl_id_t)vid); while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){ const char *zTag = fsl_stmt_g_text(&q, 0, NULL); rc = fsl_deck_T_add(d, FSL_TAGTYPE_CANCEL, NULL, zTag, "Cancelled by branch."); } fsl_stmt_finalize(&q); return rc; } #if 0 static int fsl_leaf_set( fsl_cx * f, fsl_id_t rid, char isLeaf ){ int rc; fsl_stmt * st = NULL; fsl_db * db = fsl_needs_repo(f); assert(db); rc = fsl_db_prepare_cached(db, &st, isLeaf ? "INSERT OR IGNORE INTO leaf(rid) VALUES(?)" : "DELETE FROM leaf WHERE rid=?"); if(!rc){ fsl_stmt_bind_id(st, 1, rid); fsl_stmt_step(st); fsl_stmt_cached_yield(st); } if(rc){ fsl_cx_uplift_db_error(f, db); } return rc; } #endif /** Checks vfile for any files (where chnged in (2,3,4,5)), i.e. having something to do with a merge. If either all of those changes are enqueued for checkin, or none of them are, then this function returns 0, otherwise it sets f's error state and returns non-0. */ static int fsl_check_for_partial_merge(fsl_cx * f){ if(!f->ckin.selectedIds.entryCount){ /* All files are considered enqueued. */ return 0; }else{ fsl_db * db = fsl_cx_db_ckout(f); int32_t counter = 0; int rc = fsl_db_get_int32(db, &counter, "SELECT COUNT(*) FROM (" #if 1 "SELECT DISTINCT fsl_is_enqueued(id)" " FROM vfile WHERE chnged IN (2,3,4,5)" #else "SELECT fsl_is_enqueued(id) isSel " "FROM vfile WHERE chnged IN (2,3,4,5) " "GROUP BY isSel" #endif ")" ); /** Result is 0 if no merged files are in vfile, 1 row if isSel is the same for all merge-modified files, and 2 if there is a mix of selected/unselected merge-modified files. */ if(!rc && (counter>1)){ assert(2==counter); rc = fsl_cx_err_set(f, FSL_RC_MISUSE, "Only Chuck Norris can commit " "a partial merge. Commit either all " "or none of it."); } return rc; } } /** Populates d with the contents for a FSL_SATYPE_CHECKIN manifest based on repository version basedOnVid. d is the deck to populate. basedOnVid must currently be f->ckout.rid OR the vfile table must be current for basedOnVid (see fsl_vfile_changes_scan() and fsl_vfile_load()). It "should" work with basedOnVid==0 but that's untested so far. opt is the options object passed to fsl_checkin_commit(). */ static int fsl_checkin_calc_manifest( fsl_cx * f, fsl_deck * d, fsl_id_t basedOnVid, fsl_checkin_opt const * opt ){ int rc; fsl_db * dbR = fsl_cx_db_repo(f); fsl_db * dbC = fsl_cx_db_ckout(f); fsl_stmt q = fsl_stmt_empty; fsl_deck dBase = fsl_deck_empty; char const * zColor; int deltaPolicy = opt->deltaPolicy; assert(d->f == f); assert(FSL_SATYPE_CHECKIN==d->type); #define RC if(rc) goto end /* assert(basedOnVid>0); */ rc = (opt->message && *opt->message) ? fsl_deck_C_set( d, opt->message, -1 ) : fsl_cx_err_set(f, FSL_RC_MISSING_INFO, "Cowardly refusing to commit with " "empty checkin comment."); RC; if(deltaPolicy!=0 && fsl_repo_forbids_delta_manifests(f)){ deltaPolicy = 0; }else if(deltaPolicy<0 && f->cache.seenDeltaManifest<=0){ deltaPolicy = 0; } { char const * zUser = opt->user ? opt->user : fsl_cx_user_get(f); rc = (zUser && *zUser) ? fsl_deck_U_set( d, zUser ) : fsl_cx_err_set(f, FSL_RC_MISSING_INFO, "Cowardly refusing to commit without " "a user name."); RC; } rc = fsl_check_for_partial_merge(f); RC; rc = fsl_deck_D_set( d, (opt->julianTime>0) ? opt->julianTime : fsl_db_julian_now(dbR) ); RC; if(opt->messageMimeType && *opt->messageMimeType){ rc = fsl_deck_N_set( d, opt->messageMimeType, -1 ); RC; } { /* F-cards */ static char const * errNoFilesMsg = "No files have changed. Cowardly refusing to commit."; static int const errNoFilesRc = FSL_RC_NOOP; fsl_deck * pBase = NULL /* baseline for delta generation purposes */; fsl_size_t szD = 0, szB = 0 /* see commentary below */; if(basedOnVid && deltaPolicy!=0){ /* Figure out a baseline for a delta manifest... */ fsl_uuid_str bUuid = NULL /* UUID for d's B-card */; rc = fsl_deck_load_rid(f, &dBase, basedOnVid, FSL_SATYPE_CHECKIN); RC; if(dBase.B.uuid){ /* dBase is a delta. Let's use its baseline for manifest generation. */ fsl_id_t const baseRid = fsl_uuid_to_rid(f, dBase.B.uuid); fsl_deck_finalize(&dBase); if(baseRid>0){ rc = fsl_deck_load_rid(f, &dBase, baseRid, FSL_SATYPE_CHECKIN); }else{ rc = fsl_cx_err_get(f, NULL, NULL); assert(0!=rc); } RC; }else{ /* dBase version is a suitable baseline. */ bUuid = fsl_rid_to_uuid(f, basedOnVid); if(!bUuid){ assert(f->error.code); rc = f->error.code; RC; } } /* MARKER(("Baseline = %d / %s\n", (int)pBase->rid, pBase->uuid)); */ assert(dBase.B.uuid || bUuid); rc = fsl_deck_B_set(d, dBase.B.uuid ? dBase.B.uuid : bUuid); fsl_free(bUuid); RC; pBase = &dBase; } rc = fsl_checkin_calc_F_cards2(f, d, pBase, basedOnVid, &szD, opt); /*MARKER(("szD=%d\n", (int)szD));*/ RC; if(basedOnVid && !szD){ rc = fsl_cx_err_set(f, errNoFilesRc, errNoFilesMsg); goto end; } szB = pBase ? pBase->F.used : 0; /* The following text was copied verbatim from fossil(1). It does not apply 100% here (because we use a slightly different manifest generation approach) but it clearly describes what's going on after the comment block.... */ /* ** At this point, two manifests have been constructed, either of ** which would work for this checkin. The first manifest (held ** in the "manifest" variable) is a baseline manifest and the second ** (held in variable named "delta") is a delta manifest. The ** question now is: which manifest should we use? ** ** Let B be the number of F-cards in the baseline manifest and ** let D be the number of F-cards in the delta manifest, plus one for ** the B-card. (B is held in the szB variable and D is held in the ** szD variable.) Assume that all delta manifests adds X new F-cards. ** Then to minimize the total number of F- and B-cards in the repository, ** we should use the delta manifest if and only if: ** ** D*D < B*X - X*X ** ** X is an unknown here, but for most repositories, we will not be ** far wrong if we assume X=3. */ ++szD /* account for the d->B card */; if(pBase){ /* For this calculation, i believe the correct approach is to simply count the F-cards, including those changed between the baseline and the delta, as opposed to only those changed in the delta itself. */ szD = 1 + d->F.used; } /* MARKER(("szB=%d szD=%d\n", (int)szB, (int)szD)); */ if(pBase && (deltaPolicy<0/*non-force-mode*/ && !(((int)(szD*szD)) < (((int)szB*3)-9)) /* ^^^ see comments above */ ) ){ /* Too small of a delta to be worth it. Re-calculate F-cards with no baseline. Maintenance reminder: i initially wanted to update vfile's status incrementally as F-cards are calculated, but this discard/retry breaks on the retry because vfile's state has been modified. Thus instead of updating vfile incrementally, we re-scan it after the checkin completes. */ fsl_deck tmp = fsl_deck_empty; /* Free up d->F using a kludge... */ tmp.F = d->F; d->F = fsl_deck_empty.F; fsl_deck_finalize(&tmp); fsl_deck_B_set(d, NULL); /* MARKER(("Delta is too big - re-calculating F-cards for a baseline.\n")); */ szD = 0; rc = fsl_checkin_calc_F_cards2(f, d, NULL, basedOnVid, &szD, opt); RC; if(basedOnVid && !szD){ rc = fsl_cx_err_set(f, errNoFilesRc, errNoFilesMsg); goto end; } } }/* F-cards */ /* parents... */ if( basedOnVid ){ char * zParentUuid = fsl_rid_to_artifact_uuid(f, basedOnVid, FSL_SATYPE_CHECKIN); if(!zParentUuid){ assert(f->error.code); rc = f->error.code ? f->error.code : fsl_cx_err_set(f, FSL_RC_NOT_FOUND, "Could not find checkin UUID " "for RID %"FSL_ID_T_PFMT".", basedOnVid); goto end; } rc = fsl_deck_P_add(d, zParentUuid) /* pedantic side-note: we could alternately transfer ownership of zParentUuid by fsl_list_append()ing it to d->P, but that would bypass e.g. any checking that routine chooses to apply. */; fsl_free(zParentUuid); /* if(!rc) rc = fsl_leaf_set(f, basedOnVid, 0); */ /* TODO: if( p->verifyDate ) checkin_verify_younger(vid, zParentUuid, zDate); */ RC; rc = fsl_db_prepare(dbC, &q, "SELECT merge FROM vmerge WHERE id=0 OR id<-2"); RC; while( FSL_RC_STEP_ROW == fsl_stmt_step(&q) ){ char *zMergeUuid; fsl_id_t const mid = fsl_stmt_g_id(&q, 0); //MARKER(("merging? %d\n", (int)mid)); if( (mid == basedOnVid) || (!f->cache.markPrivate && fsl_content_is_private(f,mid))){ continue; } zMergeUuid = fsl_rid_to_uuid(f, mid) /* FIXME? Adjust the query to join on blob and return the UUID? */ ; //MARKER(("merging %d %s\n", (int)mid, zMergeUuid)); if(zMergeUuid){ rc = fsl_deck_P_add(d, zMergeUuid); fsl_free(zMergeUuid); } RC; /* TODO: if( p->verifyDate ) checkin_verify_younger(mid, zMergeUuid, zDate); */ } fsl_stmt_finalize(&q); } { /* Q-cards... */ rc = fsl_db_prepare(dbR, &q, "SELECT " "CASE vmerge.id WHEN -1 THEN '+' ELSE '-' END || mhash," " merge" " FROM vmerge" " WHERE (vmerge.id=-1 OR vmerge.id=-2)" " ORDER BY 1"); while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){ fsl_id_t const mid = fsl_stmt_g_id(&q, 1); if( mid != basedOnVid ){ const char *zCherrypickUuid = fsl_stmt_g_text(&q, 0, NULL); int const qType = '+'==*(zCherrypickUuid++) ? 1 : -1; rc = fsl_deck_Q_add( d, qType, zCherrypickUuid, NULL ); } } fsl_stmt_finalize(&q); RC; } zColor = opt->bgColor; if(opt->branch && *opt->branch){ char * sym = fsl_mprintf("sym-%s", opt->branch); if(!sym){ rc = FSL_RC_OOM; goto end; } rc = fsl_deck_T_add( d, FSL_TAGTYPE_PROPAGATING, NULL, sym, NULL ); fsl_free(sym); RC; if(opt->bgColor && *opt->bgColor){ zColor = NULL; rc = fsl_deck_T_add( d, FSL_TAGTYPE_PROPAGATING, NULL, "bgcolor", opt->bgColor); RC; } rc = fsl_deck_T_add( d, FSL_TAGTYPE_PROPAGATING, NULL, "branch", opt->branch ); RC; if(basedOnVid){ rc = fsl_cancel_sym_tags(d, basedOnVid); } } if(zColor && *zColor){ /* One-shot background color */ rc = fsl_deck_T_add( d, FSL_TAGTYPE_ADD, NULL, "bgcolor", opt->bgColor); RC; } if(opt->closeBranch){ rc = fsl_deck_T_add( d, FSL_TAGTYPE_ADD, NULL, "closed", *opt->closeBranch ? opt->closeBranch : NULL); RC; } { /* Close any INTEGRATE merges if !op->integrate, or type-0 and integrate merges if opt->integrate. */ rc = fsl_db_prepare(dbC, &q, "SELECT mhash, merge FROM vmerge " " WHERE id %s ORDER BY 1", opt->integrate ? "IN(0,-4)" : "=(-4)"); while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){ fsl_id_t const rid = fsl_stmt_g_id(&q, 1); //MARKER(("Integrating %d? opt->integrate=%d\n",(int)rid, opt->integrate)); if( fsl_rid_is_leaf(f, rid) && !fsl_db_exists(dbR, /* Is not closed already... */ "SELECT 1 FROM tagxref " "WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT " AND tagtype>0", FSL_TAGID_CLOSED, rid)){ const char *zIntegrateUuid = fsl_stmt_g_text(&q, 0, NULL); //MARKER(("Integrating %d %s\n",(int)rid, zIntegrateUuid)); rc = fsl_deck_T_add( d, FSL_TAGTYPE_ADD, zIntegrateUuid, "closed", "Closed by integrate-merge." ); } } fsl_stmt_finalize(&q); RC; } end: #undef RC fsl_stmt_finalize(&q); fsl_deck_finalize(&dBase); assert(NULL==d->B.baseline || &dBase==d->B.baseline); d->B.baseline = NULL /* if it was set, it was &dBase */; if(rc && !f->error.code){ if(dbR->error.code) fsl_cx_uplift_db_error(f, dbR); else if(dbC->error.code) fsl_cx_uplift_db_error(f, dbC); else if(f->dbMain->error.code) fsl_cx_uplift_db_error(f, f->dbMain); } return rc; } int fsl_checkin_T_add2( fsl_cx * f, fsl_card_T * t){ return fsl_deck_T_add2( &f->ckin.mf, t ); } int fsl_checkin_T_add( fsl_cx * f, fsl_tagtype_e tagType, fsl_uuid_cstr uuid, char const * name, char const * value){ return fsl_deck_T_add( &f->ckin.mf, tagType, uuid, name, value ); } /** Returns true if the given blob RID is has a "closed" tag. This is generally intended only to be passed the RID of the current checkout, before attempting to perform a commit against it. */ static bool fsl_leaf_is_closed(fsl_cx * f, fsl_id_t rid){ fsl_db * const dbR = fsl_needs_repo(f); return dbR ? fsl_db_exists(dbR, "SELECT 1 FROM tagxref" " WHERE tagid=%d " " AND rid=%"FSL_ID_T_PFMT" AND tagtype>0", FSL_TAGID_CLOSED, rid) : false; } /** Returns true if the given name is the current branch for the given checkin version. */ static bool fsl_is_current_branch(fsl_db * dbR, fsl_id_t vid, char const * name){ return fsl_db_exists(dbR, "SELECT 1 FROM tagxref" " WHERE tagid=%d AND rid=%"FSL_ID_T_PFMT " AND tagtype>0" " AND value=%Q", FSL_TAGID_BRANCH, vid, name); } int fsl_checkin_commit(fsl_cx * f, fsl_checkin_opt const * opt, fsl_id_t * newRid, fsl_uuid_str * newUuid ){ int rc; fsl_deck deck = fsl_deck_empty; fsl_deck *d = &deck; fsl_db * dbC; fsl_db * dbR; char inTrans = 0; char oldPrivate; int const oldFlags = f ? f->flags : 0; fsl_id_t const vid = f ? f->ckout.rid : 0; if(!f || !opt) return FSL_RC_MISUSE; else if(!(dbC = fsl_needs_ckout(f))) return FSL_RC_NOT_A_CKOUT; else if(!(dbR = fsl_needs_repo(f))) return FSL_RC_NOT_A_REPO; assert(vid>=0); /** Do not permit a checkin to a closed leaf unless opt->branch would switch us to a new branch. */ if( fsl_leaf_is_closed(f, vid) && (!opt->branch || !*opt->branch || fsl_is_current_branch(dbR, vid, opt->branch))){ return fsl_cx_err_set(f, FSL_RC_ACCESS, "Only Chuck Norris can commit to " "a closed leaf."); } if(vid && opt->scanForChanges){ /* We need to ensure this state is current in order to determine whether a given file is locally modified vis-a-vis the commit-time vfile state. */ rc = fsl_vfile_changes_scan(f, vid, 0); if(rc) return rc; } fsl_cx_err_reset(f) /* avoid propagating an older error by accident. Did that in test code. */; oldPrivate = f->cache.markPrivate; if(opt->isPrivate || fsl_content_is_private(f, vid)){ f->cache.markPrivate = 1; } #define RC if(rc) goto end fsl_deck_init(f, d, FSL_SATYPE_CHECKIN); rc = fsl_db_transaction_begin(dbR); RC; inTrans = 1; if(f->ckin.mf.T.used){ /* Transfer accumulated tags. */ assert(!f->ckin.mf.content.used); d->T = f->ckin.mf.T; f->ckin.mf.T = fsl_deck_empty.T; } rc = fsl_checkin_calc_manifest(f, d, vid, opt); RC; if(!d->F.used){ rc = fsl_cx_err_set(f, FSL_RC_NOOP, "Cowardly refusing to generate an empty commit."); RC; } if(opt->calcRCard) f->flags |= FSL_CX_F_CALC_R_CARD; else f->flags &= ~FSL_CX_F_CALC_R_CARD; rc = fsl_deck_save( d, opt->isPrivate ); RC; assert(d->rid>0); /* Now get vfile back into shape. We do not do a vfile scan because that loses state like add/rm-queued files. */ rc = fsl_db_exec_multi(dbC, "DELETE FROM vfile WHERE vid<>" "%" FSL_ID_T_PFMT ";" "UPDATE vfile SET vid=%" FSL_ID_T_PFMT ";" "DELETE FROM vfile WHERE deleted AND " "fsl_is_enqueued(id); " "UPDATE vfile SET rid=mrid, mhash=NULL, " "chnged=0, deleted=0, origname=NULL " "WHERE fsl_is_enqueued(id)", vid, d->rid); if(!rc) rc = fsl__ckout_version_write(f, d->rid, NULL); RC; assert(d->f == f); rc = fsl_checkin_add_unsent(f, d->rid); RC; rc = fsl__ckout_clear_merge_state(f, true); RC; /* todo(?) from fossil(1) follows. Most of this seems to be what the vfile handling does (above). db_multi_exec("PRAGMA %s.application_id=252006673;", db_name("repository")); db_multi_exec("PRAGMA %s.application_id=252006674;", db_name("localdb")); // Update the vfile and vmerge tables db_multi_exec( "DELETE FROM vfile WHERE (vid!=%d OR deleted) AND is_selected(id);" "DELETE FROM vmerge;" "UPDATE vfile SET vid=%d;" "UPDATE vfile SET rid=mrid, chnged=0, deleted=0, origname=NULL" " WHERE is_selected(id);" , vid, nvid ); db_lset_int("checkout", nvid); // Update the isexe and islink columns of the vfile table db_prepare(&q, "UPDATE vfile SET isexe=:exec, islink=:link" " WHERE vid=:vid AND pathname=:path AND (isexe!=:exec OR islink!=:link)" ); db_bind_int(&q, ":vid", nvid); pManifest = manifest_get(nvid, CFTYPE_MANIFEST, 0); manifest_file_rewind(pManifest); while( (pFile = manifest_file_next(pManifest, 0)) ){ db_bind_int(&q, ":exec", pFile->zPerm && strstr(pFile->zPerm, "x")); db_bind_int(&q, ":link", pFile->zPerm && strstr(pFile->zPerm, "l")); db_bind_text(&q, ":path", pFile->zName); db_step(&q); db_reset(&q); } db_finalize(&q); */ if(opt->dumpManifestFile){ FILE * out; /* MARKER(("Dumping generated manifest to file [%s]:\n", opt->dumpManifestFile)); */ out = fsl_fopen(opt->dumpManifestFile, "w"); if(out){ rc = fsl_deck_output( d, fsl_output_f_FILE, out ); fsl_fclose(out); }else{ rc = fsl_cx_err_set(f, FSL_RC_IO, "Could not open output " "file for writing: %s", opt->dumpManifestFile); } RC; } if(d->P.used){ /* deltify the parent manifest */ char const * p0 = (char const *)d->P.list[0]; fsl_id_t const prid = fsl_uuid_to_rid(f, p0); /* MARKER(("Deltifying parent manifest #%d...\n", (int)prid)); */ assert(p0); assert(prid>0); rc = fsl__content_deltify(f, prid, d->rid, 0); RC; } end: f->flags = oldFlags; #undef RC f->cache.markPrivate = oldPrivate; /* fsl_buffer_reuse(&f->fileContent); */ if(inTrans){ if(rc) fsl_db_transaction_rollback(dbR); else{ rc = fsl_db_transaction_commit(dbR); if(!rc){ if(newRid) *newRid = d->rid; if(newUuid){ if(NULL==(*newUuid = fsl_rid_to_uuid(f, d->rid))){ rc = FSL_RC_OOM; } } } } } if(rc && !f->error.code){ if(f->dbMain->error.code) fsl_cx_uplift_db_error(f, f->dbMain); else f->error.code = rc; } fsl_checkin_discard(f); fsl_deck_finalize(d); return rc; } #undef MARKER