/* -*- 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 checkin-level APIS. */ #include #include "fossil-scm/fossil-internal.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 * f, char const * zRelName, fsl_id_t parentRid, fsl_id_t *rid, fsl_uuid_str * uuid ){ fsl_buffer * nbuf = &f->scratch; fsl_size_t const oldSize = nbuf->used; fsl_buffer * fbuf = &f->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) return rc; 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; } 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_yield_file_buffer(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){ fsl_stmt st = fsl_stmt_empty; fsl_db * db = fsl_needs_checkout(f); int rc; if(!db) return FSL_RC_NOT_A_CHECKOUT; else if(zName && *zName){ char const * zCollation = fsl_cx_filename_collation(f); rc = fsl_db_prepare(db, &st, "SELECT id FROM vfile WHERE vid=%"FSL_ID_T_PFMT " AND (pathname=%Q %s " "OR (pathname>'%q/' %s AND pathname<'%q0' %s))", (fsl_id_t)vid, zName, zCollation, zName, zCollation, zName, zCollation); }else{ rc = fsl_db_prepare(db, &st, "SELECT id FROM vfile WHERE vid=%"FSL_ID_T_PFMT, (fsl_id_t)vid); } while(!rc && (FSL_RC_STEP_ROW == fsl_stmt_step(&st))){ rc = fsl_id_bag_insert( dest, fsl_stmt_g_id(&st, 0) ); } if(FSL_RC_STEP_DONE==rc) rc = 0; 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, char const * zName, fsl_id_t * vfid ){ fsl_stmt * st = NULL; fsl_db * db = fsl_needs_checkout(f); int rc; assert(db); if(!db) return FSL_RC_NOT_A_CHECKOUT; rc = fsl_db_prepare_cached(db, &st, "SELECT id FROM vfile WHERE pathname=? %s", fsl_cx_filename_collation(f)); if(!rc){ rc = fsl_stmt_bind_text(st, 1, zName, -1, 0); if(!rc && (FSL_RC_STEP_ROW == fsl_stmt_step(st))){ *vfid = fsl_stmt_g_id(st, 0); }else{ *vfid = 0; } fsl_stmt_cached_yield(st); } if(rc && !f->error.code && db->error.code){ fsl_cx_uplift_db_error(f, db); } return rc; } int fsl_checkin_file_enqueue(fsl_cx * f, char const * zName, char relativeToCwd){ if(!f || !zName || !*zName) return FSL_RC_MISUSE; else if(!fsl_needs_checkout(f)) return FSL_RC_NOT_A_CHECKOUT; else{ fsl_buffer canon = fsl_buffer_empty; int rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &canon); if(!rc){ fsl_id_t vfid = 0; char const * path = fsl_buffer_cstr(&canon); char dirCheck; fsl_buffer * buf = &f->fsScratch; fsl_size_t const oldBagSize = f->ckin.selectedIds.entryCount; assert(!buf->used && "Misuse of f->fsScratch"); rc = fsl_filename_to_vfile_id(f, path, &vfid); if(rc) goto out; else if(vfid>0 && fsl_id_bag_contains(&f->ckin.selectedIds, vfid)){ /* A single file matching a vfile entry... */ rc = 0; }else{ /* Try interpreting it as a directory... */ while('/' == canon.mem[canon.used-1]){ assert(canon.used); canon.mem[--canon.used] = 0; assert(canon.used>0); } rc = fsl_buffer_appendf(buf, "%s%b", f->ckout.dir, &canon); if(rc) goto out; dirCheck = fsl_dir_check(fsl_buffer_cstr(buf)); rc = fsl_filename_to_vfile_ids(f, f->ckout.rid, &f->ckin.selectedIds, path); if(!rc && (dirCheck<=0)){ /* TODO: distinguish between "fossil knows nothing about" and "file not found." Heuristic: if file exists but it's not in vfile, report FSL_RC_UNKNOWN_RESOURCE, otherwise FSL_RC_NOT_FOUND. */ if( oldBagSize == f->ckin.selectedIds.entryCount ){ char const * msg; if(dirCheck<0/* it's a non-directory entry in the filesystem */){ rc = FSL_RC_UNKNOWN_RESOURCE /* it's presumably not in vfile */; msg = "Not found in repository db: %b"; }else{ rc = FSL_RC_NOT_FOUND; msg = "File not found: %b"; } rc = fsl_cx_err_set(f, rc, msg, &canon); } } } out: buf->used = 0; } fsl_buffer_clear(&canon); return rc; } } /** Internal helper which assumes path is a subdirectory name or NULL/empty (meaning the whole repo). Dequeues all matching files. Returns 0 on success. */ static int fsl_checkin_dequeue_files(fsl_cx * f, char const * path){ int rc; fsl_id_bag list = fsl_id_bag_empty; rc = fsl_filename_to_vfile_ids(f, f->ckout.rid, &list, path); if(!rc && list.entryCount){ fsl_id_t nid; for( nid = fsl_id_bag_first(&list); nid; nid = fsl_id_bag_next(&list, nid)){ fsl_id_bag_remove(&f->ckin.selectedIds, nid); } } fsl_id_bag_clear(&list); return rc; } int fsl_checkin_file_dequeue(fsl_cx * f, char const * zName, char relativeToCwd){ fsl_db * db; int rc; if(!f) return FSL_RC_MISUSE; else if(!(db = fsl_needs_checkout(f))) return FSL_RC_NOT_A_CHECKOUT; else if(!f->ckin.selectedIds.entryCount) return FSL_RC_NOT_FOUND; else if(zName && *zName){ fsl_buffer canon = fsl_buffer_empty; char const * path; rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &canon); if(!rc){ fsl_id_t vfid = 0; path = fsl_buffer_cstr(&canon); rc = fsl_filename_to_vfile_id(f, path, &vfid); /* FIXME: work on directories! */ if(vfid/* && fsl_id_bag_contains(&f->ckin.selectedIds, vfid)*/){ fsl_id_bag_remove(&f->ckin.selectedIds, vfid); }else if(!rc){ /* Assume a directory/path (but we don't bother creating the absolute path to confirm that). */ rc = fsl_checkin_dequeue_files(f, path); } fsl_buffer_clear(&canon); } }else{ rc = fsl_checkin_dequeue_files(f, NULL); } return rc; } char fsl_checkin_file_is_enqueued(fsl_cx * f, char const * zName, char relativeToCwd){ /* TODO: rewrite this to use f->ckin.selectedIds */ fsl_db * db; if(!f || !zName || !*zName) return FSL_RC_MISUSE; else if(!(db = fsl_needs_checkout(f))) return FSL_RC_NOT_A_CHECKOUT; else if(!f->ckin.selectedIds.entryCount){ /* Behave like fsl_is_enqueued() SQL function. */ return 1; } else { char rv = 0; fsl_buffer canon = fsl_buffer_empty; int rc = fsl_checkout_filename_check(f, relativeToCwd, zName, &canon); if(!rc){ fsl_id_t vfid = 0; rc = fsl_filename_to_vfile_id(f, (char const *)canon.mem, &vfid); rv = (rc && (vfid>0)) ? 0 : ((vfid>0) ? fsl_id_bag_contains(&f->ckin.selectedIds, vfid)/*asserts that arg2!=0*/ : 0); } fsl_buffer_clear(&canon); return rv; } } void fsl_checkin_discard(fsl_cx * f){ if(f){ fsl_id_bag_clear(&f->ckin.selectedIds); fsl_deck_finalize(&f->ckin.mf); } } /** 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. 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." Currently it generates a baseline manifest (with a B-card, which isn't all that cool), but it will be changed so that if pBaseline is not NULL, only differences are emitted as F-cards. 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){ int rc = 0; fsl_db * dbR = fsl_needs_repo(f); fsl_db * dbC = fsl_needs_checkout(f); 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_CHECKOUT; 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_vfile_changes_scan(f, vid, FSL_VFILE_CKSIG_NONE); 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); #if 0 fsl_id_t const vfid = fsl_stmt_g_id(q,1); 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_file_perm_t 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. This indicates files deleted somewhere between baseline manifest and the delta. Depending on which query we use, it can also pickup deleted/selected files. The problem with that is this 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. */ #if 0 /* only need this if we use the historical query */ if(fsl_checkin_file_is_enqueued(f, pFile->name, 0)){ /* Workaround for the above corner-case. Why is it that Richard's original fossil(1) algos never need such workarounds? Something to ponder. LOL. And now we need to change the query anyway to pick up deleted/selected entries (just for counting purposes!) when generating a baseline (which never hits this block). Another potential corner case: f-rm file1 file3 f-checkin ... by default all files are selected. Let's assume file2 was deleted somewhere between the baseline and checkout. is-enqueued will say "true" because an empy checkin list is the same as a full checkin list. So we'll count them as changes here even though we should not. To work around that we have to know if vfile contains pFile->name. And then there are probably other renaming-related corner cases. */ fsl_id_t idCheck = 0; fsl_checkout_filename_vfile_id(f, pFile->name, 0, &idCheck); if(idCheck>0){ ++changeCounter; MARKER(("Deletion change-counter kludge: %s\n", pFile->name)); } } #endif rc = fsl_deck_F_add(d, pFile->name, NULL, pFile->perm, NULL); if(rc) break; fsl_deck_F_next(pBaseline, &pFile); } 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, &fileBlobRid, &fUuid); RC; /* MARKER(("New content: %d / %s / %s\n", (int)fileBlobRid, fUuid, zName)); */ if(0 != fsl_uuidcmp(zUuid, fUuid)){ zUuid = fUuid; } }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); 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_checkin_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_checkout(f); fsl_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, "Cannot commit only part of a merge. " "Commit either all none of it."); } return rc; } } /** Populates d with the contents for a FSL_CATYPE_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_from_rid()). 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_checkout(f); fsl_stmt q = fsl_stmt_empty; fsl_deck dBase = fsl_deck_empty; char const * zColor; assert(d->f == f); assert(FSL_CATYPE_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_RANGE, "Cowardly refusing to commit with " "empty checkin comment."); RC; { char const * zUser = opt->user ? opt->user : fsl_cx_user_get(f); rc = (zUser && *zUser) ? fsl_deck_U_set( d, zUser, -1 ) : fsl_cx_err_set(f, FSL_RC_RANGE, "Cowardly refusing to commit without " "a user name."); RC; } rc = fsl_checkin_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_RANGE; fsl_deck * pBase = NULL /* baseline for delta generation purposes */; fsl_size_t szD = 0, szB = 0 /* see commentary below */; if(basedOnVid && opt->deltaPolicy!=0){ /* Figure out a baseline for a delta manifest... */ rc = fsl_deck_load_rid(f, &dBase, basedOnVid, FSL_CATYPE_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); assert(baseRid>0); rc = fsl_deck_load_rid(f, &dBase, baseRid, FSL_CATYPE_CHECKIN); RC; }else{ /* dBase version is a suitable baseline. */ } pBase = &dBase; /* MARKER(("Baseline = %d / %s\n", (int)pBase->rid, pBase->uuid)); */ rc = fsl_deck_B_set(d, pBase->uuid); RC; } rc = fsl_checkin_calc_F_cards2(f, d, pBase, basedOnVid, &szD); /* MARKER(("szD=%d\n", (int)szD)); */ RC; if(basedOnVid && !szD){ rc = fsl_cx_err_set(f, errNoFilesRc, errNoFilesMsg); goto end; } szB = pBase ? pBase->F.list.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.list.used; } /* MARKER(("szB=%d szD=%d\n", (int)szB, (int)szD)); */ if(pBase && (opt->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); 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_CATYPE_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".", (fsl_id_t)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 id, 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 recId = fsl_stmt_g_id(&q, 0); fsl_id_t const mid = fsl_stmt_g_id(&q, 1); 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? */ ; assert(zMergeUuid); rc = fsl_deck_P_add(d, zMergeUuid); fsl_free(zMergeUuid); if(!rc){ /* Is this right? */ fsl_db_exec(dbC, "DELETE FROM vmerge WHERE id=%"FSL_ID_T_PFMT, (fsl_id_t)recId); } RC; /* TODO: if( p->verifyDate ) checkin_verify_younger(mid, zMergeUuid, zDate); */ } fsl_stmt_finalize(&q); } { /* Q-cards... */ /* UNTESTED! */ rc = fsl_db_prepare(dbR, &q, "SELECT CASE vmerge.id WHEN -1 THEN '+' ELSE '-' END AS type," " blob.uuid, merge" " FROM vmerge, blob" " WHERE (vmerge.id=-1 OR vmerge.id=-2)" " AND blob.rid=vmerge.merge" " ORDER BY 1"); while( !rc && (FSL_RC_STEP_ROW==fsl_stmt_step(&q)) ){ fsl_id_t const mid = fsl_stmt_g_id(&q, 2); if( mid != basedOnVid ){ const char *zType = fsl_stmt_g_text(&q, 0, NULL); const char *zCherrypickUuid = fsl_stmt_g_text(&q, 1, NULL); assert(zType); assert(zCherrypickUuid); rc = fsl_deck_Q_add( d, *zType, 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; } { /* UNTESTED! Close any INTEGRATE merges if !op->integrate, or type-0 and integrate merges if opt->integrate. */ rc = fsl_db_prepare(dbC, &q, "SELECT uuid, merge FROM vmerge JOIN blob ON merge=rid" " 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); 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, (fsl_id_t)rid)){ const char *zIntegrateUuid = fsl_stmt_g_text(&q, 0, NULL); assert(zIntegrateUuid); if(!zIntegrateUuid){ rc = FSL_RC_OOM; goto end; } 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); d->B.baseline = NULL /* if it was set, it was &dBase */; if(rc && !f->error.code){ if(d->error.code){ fsl_error_move(&d->error, &f->error); }else if(dbR->error.code){ fsl_cx_uplift_db_error(f, dbR); }else if(dbC->error.code){ fsl_cx_uplift_db_error(f, dbC); } } return rc; } int fsl_checkin_T_add( fsl_cx * f, fsl_tag_type tagType, fsl_uuid_cstr uuid, char const * name, char const * value){ return f ? fsl_deck_T_add( &f->ckin.mf, tagType, uuid, name, value ) : FSL_RC_MISUSE; } 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_checkout(f))) return FSL_RC_NOT_A_CHECKOUT; else if(!(dbR = fsl_needs_repo(f))) return FSL_RC_NOT_A_REPO; assert(vid>=0); if( fsl_db_exists(dbR, "SELECT 1 FROM tagxref" " WHERE tagid=%d " " AND rid=%"FSL_ID_T_PFMT" AND tagtype>0", FSL_TAGID_CLOSED, (fsl_id_t)vid) ){ return fsl_cx_err_set(f, FSL_RC_MISUSE, "Cannot commit against a closed leaf."); } 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_CATYPE_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(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); assert(d->uuid); /* We rescan the vfile, rather than just fiddle with the existing one, because our throw-away-and-re-calculate-F-cards bits must either do the updating there (which breaks when we re-calculate them) or we must somehow know exactly which entries to touch here. */ rc = fsl_vfile_changes_scan(f, d->rid, FSL_VFILE_CKSIG_CLEAR_VFILE); if(!rc){ rc = fsl_config_set_id(f, FSL_CONFDB_CKOUT, "checkout", d->rid); if(!rc) rc = fsl_cx_update_checkout_uuid(f); } RC; 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, &f->error ); 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_reset(&f->fileContent); */ if(inTrans){ if(rc) fsl_db_transaction_rollback(dbR); else{ rc = fsl_db_transaction_commit(dbR); if(!rc){ #if 1 fsl_repo_leaves_rebuild(f) /* (Partial!) KLUDGE/workaround for my lack of knowledge about how to efficiently update the leaf entries. Hmmm. Has no effect. Hmm. No... it's marking the previous parent as the leaf. Hmmm. Better than having them all be leaves, for the time being :/. A 'fossil rebuild' fixes this, indicating that our metadata is correct, but hinting that part of libfossil's crosslinking is missing a piece. */; #endif if(newRid) *newRid = d->rid; if(newUuid){ *newUuid = d->uuid; d->uuid = NULL /* transfer ownership */; } } } } if(rc && !f->error.code){ if(d->error.code) fsl_error_move(&d->error, &f->error); else if(dbR->error.code) fsl_cx_uplift_db_error(f, dbR); } fsl_checkin_discard(f); fsl_deck_finalize(d); return rc; } #undef MARKER