/* -*- 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 <assert.h>
#include "fossil-scm/fossil-internal.h"
/* Only for debugging */
#include <stdio.h>
#define MARKER(pfexp) \
do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \
printf pfexp; \
} while(0)
/**
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