/* -*- 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 <assert.h>
#include "fossil-scm/internal.h"
#include "fossil-scm/checkout.h"
#include "fossil-scm/confdb.h"
/* Only for debugging */
#include <stdio.h>
#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 * 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 * 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