/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/*
Copyright (c) 2014 D. Richard Hipp
This program is free software; you can redistribute it and/or
modify it under the terms of the Simplified BSD License (also
known as the "2-Clause License" or "FreeBSD License".)
This program is distributed in the hope that it will be useful,
but without any warranty; without even the implied warranty of
merchantability or fitness for a particular purpose.
Author contact information:
drh@hwaci.com
http://www.hwaci.com/drh/
*****************************************************************************
This file houses the code for checkout-level APIS.
*/
#include <assert.h>
#include "fossil-scm/fossil-internal.h"
/* Only for debugging */
#include <stdio.h>
#define MARKER(pfexp) \
do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \
printf pfexp; \
} while(0)
/**
Kludge for type-safe strncmp/strnicmp inconsistency.
*/
static int fsl_strnicmp_int(char const *zA, char const * zB, fsl_size_t nByte){
return fsl_strnicmp( zA, zB, (fsl_int_t)nByte);
}
static int fsl_cx_init_fsscratch(fsl_cx * f){
enum {
/* because testing shows a lot of re-allocs via some of the lower-level
stat()-related bits, we pre-allocate this many bytes into f->fsScratch.
Curiously, i see almost no difference in (re)allocation behaviour until i
increase this to over 200.
*/
InitialScratchCapacity = 512
};
int rc = 0;
if(f->fsScratch.capacity < InitialScratchCapacity){
rc = fsl_buffer_reserve(&f->fsScratch, InitialScratchCapacity);
assert(rc || (f->fsScratch.capacity >= InitialScratchCapacity));
}
return rc;
}
int fsl_checkout_filename_check( fsl_cx * f, char relativeToCwd,
char const * zOrigName, fsl_buffer * pOut ){
int rc;
if(!f || !zOrigName || !*zOrigName) return FSL_RC_MISUSE;
else if(!f->ckout.dir
|| !fsl_needs_checkout(f)/* will update f's error state*/
) return FSL_RC_NOT_A_CHECKOUT;
#if 0
/* Is this sane? */
else if(fsl_is_simple_pathname(zOrigName,1)){
rc = 0;
if(pOut){
rc = fsl_buffer_append(pOut, zOrigName, fsl_strlen(zOrigName));
}
}
#endif
else{
char const * zLocalRoot;
char const * zFull;
fsl_size_t nLocalRoot;
fsl_size_t nFull;
fsl_buffer * full = &f->fsScratch;
int (*xCmp)(char const *, char const *,fsl_size_t);
char endsWithSlash;
assert((pOut != full) && "Misuse of f->fsScratch!");
assert(full->used==0 && "Someone did not re-set f->fsScratch OR it is in use higher up the stack.");
if(!full->mem){
rc = fsl_cx_init_fsscratch(f);
if(rc) goto end;
}
zLocalRoot = f->ckout.dir;
assert(zLocalRoot);
assert(*zLocalRoot);
nLocalRoot = fsl_strlen(f->ckout.dir);
assert(nLocalRoot);
assert('/' == zLocalRoot[nLocalRoot-1]);
full->used = 0;
rc = fsl_file_canonical_name2(relativeToCwd ? NULL : zLocalRoot,
zOrigName, full, 1);
#if 0
MARKER(("canon2: %p (%s) %s ==> %s\n", (void const *)full->mem,
relativeToCwd ? "cwd" : "ckout", zOrigName, fsl_buffer_cstr(full)));
#endif
if(rc){
if(FSL_RC_OOM != rc){
rc = fsl_cx_err_set(f, rc, "Error #%d (%s) canonicalizing "
"file name: %s\n",
rc, fsl_rc_cstr(rc),
zOrigName);
}
goto end;
}
zFull = fsl_buffer_cstr2(full, &nFull);
xCmp = fsl_cx_is_case_sensitive(f)
? fsl_strncmp
: fsl_strnicmp_int;
assert(zFull);
assert(nFull>0);
endsWithSlash = '/' == zFull[nFull-1];
if( ((nFull==nLocalRoot-1 || (nFull==nLocalRoot && endsWithSlash))
&& xCmp(zLocalRoot, zFull, nFull)==0)
|| (nFull==1 && zFull[0]=='/' && nLocalRoot==1 && zLocalRoot[0]=='/') ){
enum { OutputDot = 1 };
/* Special case. zOrigName refers to zLocalRoot directory.
Outputing "." instead of nothing is a historical decision
which may be worth re-evaluating. Currently fsl_cx_stat() relies
on it.
*/
if(pOut){
char const * zOut;
fsl_size_t nOut;
if(endsWithSlash){ /* retain trailing slash */
zOut = "./";
nOut = 2;
}else{
zOut = ".";
nOut = 1;
};
rc = fsl_buffer_append(pOut, zOut, nOut);
}else{
rc = 0;
}
goto end;
}
if( nFull<=nLocalRoot || xCmp(zLocalRoot, zFull, nLocalRoot) ){
rc = fsl_cx_err_set(f, FSL_RC_RANGE,
"File is outside of checkout tree: %s",
zOrigName);
goto end;
}
#if 0
/*
This impl leads to inconsistencies in handling of ./foo/bar vs ./foo/bar/
*/
if(!fsl_is_simple_pathname(zFull+nLocalRoot, 1)){
rc = fsl_cx_err_set(f, FSL_RC_RANGE,
"Filename component does not resolve to "
"a \"simple filename\": %s", zFull+nLocalRoot);
}else if(pOut){
rc = fsl_buffer_append(pOut, zFull + nLocalRoot, nFull - nLocalRoot);
}
#else
if(pOut){
rc = fsl_buffer_append(pOut, zFull + nLocalRoot, nFull - nLocalRoot);
}
#endif
end:
full->used = 0;
}
return rc;
}
/**
Returns a fsl_checkout_change_t value for the given
fsl_vfile_change_t value.
Why are these not consolidated into one enum?
*/
static fsl_checkout_change_t fsl_vfile_to_ckout(int vChange){
switch(vChange){
case FSL_VFILE_CHANGE_NONE: return FSL_CKOUT_CHANGE_NONE;
case FSL_VFILE_CHANGE_MOD: return FSL_CKOUT_CHANGE_MOD;
case FSL_VFILE_CHANGE_MERGE_MOD: return FSL_CKOUT_CHANGE_MERGE_MOD;
case FSL_VFILE_CHANGE_MERGE_ADD: return FSL_CKOUT_CHANGE_MERGE_ADD;
case FSL_VFILE_CHANGE_INTEGRATE_MOD: return FSL_CKOUT_CHANGE_INTEGRATE_MOD;
case FSL_VFILE_CHANGE_INTEGRATE_ADD: return FSL_CKOUT_CHANGE_INTEGRATE_ADD;
default:
return FSL_CKOUT_CHANGE_NONE;
}
}
int fsl_checkout_changes_visit( fsl_cx * f, fsl_id_t vid,
char doScan,
fsl_checkout_changes_f visitor,
void * state ){
int rc;
fsl_db * db;
fsl_stmt st = fsl_stmt_empty;
int count = 0;
fsl_checkout_change_t change;
fsl_fstat fstat;
if(!f || !visitor) return FSL_RC_MISUSE;
db = fsl_needs_checkout(f);
if(!db) return FSL_RC_NOT_A_CHECKOUT;
if(vid<0){
vid = f->ckout.rid;
assert(vid>=0);
}
if(doScan){
rc = fsl_vfile_changes_scan(f, vid, 0);
if(rc) goto end;
}
rc = fsl_db_prepare(db, &st,
"SELECT chnged, deleted, rid, pathname, origname "
"FROM vfile WHERE vid=%"FSL_ID_T_PFMT,
(fsl_id_t)vid);
assert(!rc);
while( FSL_RC_STEP_ROW == fsl_stmt_step(&st) ){
int const changed = fsl_stmt_g_int32(&st, 0);
int const deleted = fsl_stmt_g_int32(&st,1);
fsl_id_t const vrid = fsl_stmt_g_id(&st,2);
char const * name;
char const * oname = NULL;
name = fsl_stmt_g_text(&st, 3, NULL);
oname = fsl_stmt_g_text(&st,4,NULL);
if(oname && (0==fsl_strcmp(name, oname))){
/* Work around a fossil oddity which sets origname=pathname
during a 'mv' operation.
*/
oname = NULL;
}
change = FSL_CKOUT_CHANGE_NONE;
if(deleted){
change = FSL_CKOUT_CHANGE_REMOVED;
}else if(0==vrid){
change = FSL_CKOUT_CHANGE_ADDED;
}else if(NULL != oname){
change = FSL_CKOUT_CHANGE_RENAMED;
}else{
fstat = fsl_fstat_empty;
if( fsl_cx_stat(f, 0, name, &fstat ) ){
change = FSL_CKOUT_CHANGE_MISSING;
fsl_cx_err_reset(f) /* keep FSL_RC_NOT_FOUND from bubbling
up to the client! */;
}else if(!changed){
continue;
}else{
change = fsl_vfile_to_ckout(changed);
}
}
assert(change);
++count;
rc = visitor(state, change, name, oname);
if(rc){
if(!f->error.code && (FSL_RC_OOM!=rc)){
fsl_cx_err_set(f, rc, "Error %s returned from changes callback.",
fsl_rc_cstr(rc));
}
break;
}
}
end:
fsl_stmt_finalize(&st);
if(rc && db->error.code && !f->error.code){
fsl_cx_uplift_db_error(f, db);
}
return rc;
}
int fsl_checkout_file_add( fsl_cx * f, char relativeToCwd, char const * zFilename ){
int rc;
fsl_db * db = fsl_needs_checkout(f);
fsl_fstat fst = fsl_fstat_empty;
fsl_buffer fname = fsl_buffer_empty;
char const * zNorm;
fsl_id_t const vid = f ? f->ckout.rid : 0;
if(!f) return FSL_RC_MISUSE;
else if(!db) return FSL_RC_NOT_A_CHECKOUT;
assert(vid>=0);
rc = fsl_cx_stat2(f, relativeToCwd, zFilename, &fst, &fname, 0);
if(rc) goto end;
zNorm = fsl_buffer_cstr(&fname);
if( fsl_db_exists(db, "SELECT 1 FROM vfile"
" WHERE vid=%"FSL_ID_T_PFMT
" AND pathname=%Q %s",
(fsl_id_t)vid, zNorm,
fsl_cx_filename_collation(f)) ){
rc = fsl_db_exec(db, "UPDATE vfile SET deleted=0,"
" mtime=%"FSL_INT64_T_PFMT
" WHERE vid=%"FSL_ID_T_PFMT
" AND pathname=%Q %s",
(fsl_int64_t)fst.mtime,
(fsl_id_t)vid, zNorm,
fsl_cx_filename_collation(f));
}else{
int const chnged = FSL_VFILE_CHANGE_MOD
/* fossil(1) sets chnged=0 on 'add'ed vfile records, but then the 'status'
command updates the field to 1. To avoid down-stream inconsistencies
(such as the ones which lead me here), we'll go ahead and set it to
1 here.
*/;
rc = fsl_db_exec(db,
"INSERT INTO "
"vfile(vid,chnged,deleted,rid,mrid,pathname,isexe,islink,mtime)"
"VALUES(%"FSL_ID_T_PFMT",%d,0,0,0,%Q,%d,%d,%"FSL_INT64_T_PFMT")",
(fsl_id_t)vid, chnged, zNorm,
(FSL_FSTAT_PERM_EXE==fst.perm) ? 1 : 0,
(FSL_FSTAT_TYPE_LINK==fst.type) ? 1 : 0,
(fsl_int64_t)fst.mtime
);
}
end:
fsl_buffer_clear(&fname);
if(rc && db->error.code && !f->error.code){
fsl_cx_uplift_db_error(f, db);
}
return rc;
}
int fsl_checkout_file_rm( fsl_cx * f, char relativeToCwd, char const * zFilename,
char dirsRecursive ){
int rc;
fsl_db * db = fsl_needs_checkout(f);
fsl_buffer fname = fsl_buffer_empty;
/* char const * zNorm; */
char const * zNorm;
char const * zCollate;
fsl_id_t const vid = f ? f->ckout.rid : 0;
if(!f) return FSL_RC_MISUSE;
else if(!db) return FSL_RC_NOT_A_CHECKOUT;
assert(vid>=0);
rc = fsl_checkout_filename_check(f, relativeToCwd, zFilename, &fname);
if(rc) goto end;
zNorm = fsl_buffer_cstr(&fname);
/* MARKER(("fsl_checkout_file_rm(%d, %s) ==> %s\n", relativeToCwd, zFilename, zNorm)); */
assert(zNorm);
if(fname.used){
/* Should this be done by fsl_checkout_filename_check()? */
/* Trim trailing slashes... */
char * tailCheck = fsl_buffer_str(&fname) + fname.used - 1;
for( ; (tailCheck>zNorm) && ('/'==*tailCheck); --tailCheck){
*tailCheck = 0;
--fname.used;
}
}
zCollate = fsl_cx_filename_collation(f);
if(dirsRecursive){
rc = fsl_db_exec(db,
"UPDATE vfile SET deleted=1 "
"WHERE vid=%"FSL_ID_T_PFMT
" AND NOT deleted"
" AND (pathname=%Q %s OR "
" (pathname>'%q/' %s AND pathname<'%q0' %s))",
(fsl_id_t)vid,
zNorm, zCollate,
zNorm, zCollate, zNorm, zCollate);
}else{
rc = fsl_db_exec(db,
"UPDATE vfile SET deleted=1 "
"WHERE vid=%"FSL_ID_T_PFMT
" AND NOT deleted"
" AND pathname=%Q %s",
(fsl_id_t)vid,
zNorm, zCollate);
}
assert(!rc);
if(!rc){
/* Remove ADDed-but-not-yet-committed entries... */
rc = fsl_db_exec(db,
"DELETE FROM vfile WHERE vid=%"FSL_ID_T_PFMT
" AND rid=0 AND deleted",
(fsl_id_t)vid);
}
end:
fsl_buffer_clear(&fname);
if(rc && db->error.code && !f->error.code){
fsl_cx_uplift_db_error(f, db);
}
return rc;
}
int fsl_checkout_changes_scan(fsl_cx * f){
return f
? fsl_vfile_changes_scan(f, -1, FSL_VFILE_CKSIG_CLEAR_VFILE)
: FSL_RC_MISUSE;
}
/* These commit/checkin bits are purely hypothetical... */
struct fsl_commit_opt {
char const * user;
char const * comment;
fsl_confirmation_f confirmer;
void * confirmerState;
fsl_list filenameList;
};
typedef struct fsl_commit_opt fsl_commit_opt;
#define fsl_commit_opt_empty_m { \
NULL/*user*/, NULL/*comment*/,\
NULL/*confirmer*/, NULL/*confirmerState*/, \
fsl_list_empty_m/*filenameList*/\
}
extern const fsl_commit_opt fsl_commit_opt_empty;
const fsl_commit_opt fsl_commit_opt_empty = fsl_commit_opt_empty_m;
int fsl_commit_pending(fsl_cx * f, fsl_commit_opt const * opt ){
int rc = FSL_RC_NYI;
fsl_deck mf = fsl_deck_empty;
/* fsl_deck dco = fsl_deck_empty; */
char const * user;
char inTrans = 0;
fsl_db * db = f ? fsl_needs_checkout(f) : NULL;
if(!f || !opt) return FSL_RC_MISUSE;
else if(!db) return FSL_RC_NOT_A_CHECKOUT;
assert(fsl_cx_db_repo(f));
assert(f->ckout.dir);
fsl_deck_init(f, &mf, FSL_CATYPE_CHECKIN);
user = (opt->user && *opt->user) ? opt->user : fsl_cx_user_get(f);
if(!user || !*user){
return fsl_cx_err_set(f, FSL_RC_MISUSE,
"Committing requires a valid user name.");
}
#define RCHECK if(rc) goto end
rc = fsl_deck_U_set(&mf, user, -1);
RCHECK;
/*
TODO:
- start transaction
- create manifest deck.
- Figure out how to calculate list of files properly for the manifest.
- Figure out how to calculate a delta manifest.
- fsl_checkout_changes_visit() and collect the list of changed files.
- fsl_content_put() each file. Update the deck as we go.
- save deck
- don't forget UNDO support?
- end transaction
- profit!
*/
#undef RCHECK
end:
if(rc && !f->error.code){
if(mf.error.code) fsl_cx_err_set_e(f, &mf.error);
else if(db->error.code) fsl_cx_uplift_db_error(f, db);
}
fsl_deck_finalize(&mf);
if(inTrans){
if(!rc){
rc = fsl_db_transaction_commit(db);
if(rc && !f->error.code){
fsl_cx_uplift_db_error(f, db);
}
}else{
fsl_db_transaction_rollback(db);
}
}
return rc;
}
int fsl_checkout_filename_vfile_id( fsl_cx * f, char const * fn, fsl_id_t vid,
fsl_id_t * rv ){
fsl_db * db = fsl_cx_db_checkout(f);
fsl_id_t fnid = 0;
fsl_stmt * qSel = NULL;
int rc;
assert(f);
assert(db);
assert(rv);
if(!fn || !fsl_is_simple_pathname(fn, 1)){
return fsl_cx_err_set(f, FSL_RC_RANGE,
"Filename is not a \"simple\" path: %s",
fn);
}
else if(vid<=0) vid = f->ckout.rid;
assert(vid>0);
*rv = 0;
rc = fsl_db_prepare_cached(db, &qSel,
"SELECT id FROM vfile "
"WHERE vid=? AND pathname=?");
if(rc){
fsl_cx_uplift_db_error(f, db);
return rc;
}
rc = fsl_stmt_bind_id(qSel, 1, vid);
if(!rc) rc = fsl_stmt_bind_text(qSel, 2, fn, -1, 0);
if(rc){
fsl_stmt_cached_yield(qSel);
}else{
rc = fsl_stmt_step(qSel);
if( FSL_RC_STEP_ROW == rc ){
rc = 0;
fnid = fsl_stmt_g_id(qSel, 0);
assert(fnid>0);
}else if(FSL_RC_STEP_DONE == rc){
rc = 0;
}
fsl_stmt_cached_yield(qSel);
}
if(!rc){
*rv = fnid;
}else if(db->error.code){
fsl_cx_uplift_db_error(f, db);
}
return rc;
}
#undef MARKER