/* -*- 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
*/
/**
f-ciwoco is a libfossil app to create checkins in a repository without
requiring a checkout. ciwoco = CheckIn WithOut CheckOut.
*/
#ifdef NDEBUG
/* Force assert() to always be in effect. */
#undef NDEBUG
#endif
#include "libfossil.h"
#include <string.h>
// Only for testing/debugging..
#define MARKER(pfexp) \
do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \
printf pfexp; \
} while(0)
// Global app state.
struct App_ {
char const * sym;
char const * branch;
char const * comment;
char const * commentFile;
char const * stripPrefix;
fsl_size_t nStripPrefix;
bool addRCard;
bool dryRun;
bool checkIgnoreGlobs;
bool noChangesNoCry;
} App = {
"trunk"/*sym*/,
NULL/*branch*/,
NULL/*comment*/,
NULL/*commentFile*/,
NULL/*stripPrefix*/,
0/*nStripPrefix*/,
true/*addRCard*/,
true/*dryRun*/,
true/*checkIgnoreGlobs*/,
false/*noChangesNoCry*/
};
/**
Takes a filename in the form NAME[:REPO-NAME] and dissects it into
two parts:
*localName is the NAME part.
*repoName is the REPO-NAME part, if set, else the NAME part.
The returned strings are owned by fcli and survive until app
shutdown.
*/
static void parse_fn(char const *full, char const ** localName,
char const ** repoName){
char * z = fcli_fax( fsl_strdup(full) );
*localName = *repoName = z;
if('.'==z[0] && '/'==z[1]){
/* For usability convenience, e.g. passing along input from:
find . -type f
Strip any leading ./ on the reponame part.
*/
z+=2;
*repoName = z;
}
while( *z && *z!=':' ) ++z;
if(':'==*z){
*z++ = 0;
*repoName = z;
}
if(App.stripPrefix){
if(0==fsl_strncmp(App.stripPrefix, *repoName, App.nStripPrefix)){
*repoName += App.nStripPrefix;
}
}
}
/**
Checks whether d has changes compared to its parent (if any).
Returns 0 on succcess (any error is considered fatal). Sets *rv to
true if there are changes, else *rv to false.
*/
static int deck_check_changes(fsl_deck * const d, bool * rv){
int rc = 0;
fsl_cx * const f = d->f;
fsl_deck p = fsl_deck_empty;
*rv = false;
if(!d->P.used){
/* New root deck. */
*rv = true;
return 0;
}
rc = fsl_deck_load_sym(f, &p, (char const *)d->P.list[0], d->type);
if(rc) goto end;
if(p.F.used != d->F.used
/* ^^^ that isn't necessarily a correct heuristic if the parent
is a delta manifest. */
|| (p.R && d->R && fsl_strcmp(p.R,d->R))){
goto change;
}
for(fsl_size_t i = 0; i < p.F.used; ++i ){
fsl_card_F const * const fL = &p.F.list[i];
fsl_card_F const * const fR = &d->F.list[i];
if(fR->priorName
|| fsl_strcmp(fL->name, fR->name)
|| fsl_strcmp(fL->uuid, fR->uuid)
|| fL->perm != fR->perm){
goto change;
}
}
end:
fsl_deck_finalize(&p);
return rc;
change:
*rv = true;
goto end;
}
/**
Does... everything. Returns 0 on success (or non-error).
*/
static int do_everything(void){
int rc = 0;
fsl_cx * const f = fcli_cx();
fsl_deck d = fsl_deck_empty;
char const *fname = 0;
fsl_buffer fcontent = fsl_buffer_empty;
int const verbose = fcli_is_verbose();
bool isNewRoot = false;
char * dBranch = NULL;
rc = fsl_cx_transaction_begin(f);
if(rc) return rc;
if(0==fsl_strcmp("-", App.sym)){
isNewRoot = true;
fsl_deck_init(f, &d, FSL_SATYPE_CHECKIN);
if(!App.branch || !*App.branch){
// We "need" a default branch, so...
App.branch = "ciwoco";
}
}else{
rc = fsl_deck_load_sym(f, &d, App.sym, FSL_SATYPE_CHECKIN);
if(rc) goto end;
assert(f==d.f);
f_out("Deriving from checkin [%s] (RID %"FSL_ID_T_PFMT")\n",
App.sym, d.rid);
rc = fsl_deck_derive(&d);
if(rc) goto end;
}
if(App.branch){
if(!isNewRoot){
rc = fsl_branch_of_rid(f, d.rid, true, &dBranch);
if(rc) goto end;
}
if(isNewRoot || 0!=fsl_strcmp(dBranch, App.branch)){
/* Only set the branch if it would really be a change. In that case,
we need to cancel the previous branch's tag. */
rc = fsl_deck_branch_set(&d, App.branch);
if(!rc && dBranch){
dBranch = fsl_mprintf("sym-%z", dBranch);
rc = fsl_deck_T_add(&d, FSL_TAGTYPE_CANCEL, NULL, dBranch,
"Cancelled by branch.");
}
if(rc) goto end;
}
}
if(App.commentFile){
rc = fsl_buffer_fill_from_filename(&fcontent, App.commentFile);
if(rc){
rc = fcli_err_set(rc, "Cannot read comment file: %s", App.commentFile);
goto end;
}
char * c = fsl_buffer_take(&fcontent);
fcli_fax(c);
App.comment = c;
}
if(!App.comment){
rc = fcli_err_set(FSL_RC_MISUSE, "Missing required checkin comment.");
goto end;
}
rc = fsl_deck_C_set(&d, App.comment, -1);
if(0==rc){
char const * u = fsl_cx_user_get(f);
if(!u){
rc = fcli_err_set(FSL_RC_MISUSE,
"Cannot determine user name. "
"Try using --user NAME.");
}else{
rc = fsl_deck_U_set(&d, u);
}
}
if(rc) goto end;
/**
Look for args in the form:
FILENAME[:REPO-NAME]
Where FILENAME is the local filesystem name and REPO-NAME is the
name of that file within the repository, defaulting to FILENAME.
Potential TODOs:
- If FILENAME is a directory, process it recursively. If so, possibly
offer a flag which tells it to only process files which already exist.
*/
while((fname = fcli_next_arg(true))){
/* Handle input files... */
char const * nameLocal = 0;
char const * nameRepo = 0;
fsl_fstat fst = fsl_fstat_empty;
parse_fn(fname, &nameLocal, &nameRepo);
if(!*nameLocal || !*nameRepo){
rc = fcli_err_set(FSL_RC_MISUSE,
"Only Chuck Norris may use empty filenames.");
goto end;
}
rc = fsl_stat(nameLocal, &fst, false);
if(rc){
rc = fcli_err_set(rc, "Error stat()'ing file: %s", nameLocal);
goto end;
}
switch(fst.type){
case FSL_FSTAT_TYPE_DIR:
rc = fcli_err_set(FSL_RC_TYPE,
"Cannot currently CiWoCo a whole directory: %s",
nameLocal);
break;
case FSL_FSTAT_TYPE_LINK:
rc = fcli_err_set(FSL_RC_TYPE,
"Symlinks are currently unsupported: %s",
nameLocal);
break;
case FSL_FSTAT_TYPE_UNKNOWN:
rc = fcli_err_set(FSL_RC_TYPE, "Unknown file type: %s", nameLocal);
break;
case FSL_FSTAT_TYPE_FILE: break;
}
if(rc) goto end;
rc = fsl_buffer_fill_from_filename(&fcontent, nameLocal);
if(rc){
rc = fcli_err_set(rc, "Error reading file: %s", nameLocal);
goto end;
}
if(App.checkIgnoreGlobs){
if(fsl_cx_glob_matches(f, FSL_GLOBS_IGNORE, nameRepo)){
if(verbose){
f_out("Skipping due to ignore-glob: %s\n", nameLocal);
}
continue;
}
}
fsl_card_F const * fc = fsl_deck_F_search(&d, nameRepo);
if(NULL==fc){
/* New file: make sure its name is not reserved. If it's reserved
but already in the repo then the damage is already done and
we won't enforce it retroactively. */
rc = fsl_reserved_fn_check(f, nameRepo, -1, false);
f_out("reserved name check: %s %s\n", fsl_rc_cstr(rc), nameRepo);
if(rc) goto end;
}
if(verbose){
char const * const action = fc ? "Updating" : "Adding";
if(nameLocal!=nameRepo){
f_out("%s [%s] as [%s]\n", action,nameLocal, nameRepo);
}else{
f_out("%s [%s]\n", action, nameLocal);
}
}
rc = fsl_deck_F_set_content(&d, nameRepo, &fcontent,
fst.perm==FSL_FSTAT_PERM_EXE
? FSL_FILE_PERM_EXE
: FSL_FILE_PERM_REGULAR,
NULL);
if(rc) goto end;
}/*next-arg loop*/
if(App.addRCard || (isNewRoot && !d.F.used)
/* A checkin artifact with no P/F/Q/R-cards cannot be unambiguously
recognized as a checkin, so we'll add an R-card if someone
creates an empty root-level checkin. */
){
if(verbose) f_out("Calculating R-card... ");
rc = fsl_deck_R_calc(&d);
if(verbose) f_out("\n");
if(rc) goto end;
}
if(isNewRoot){
f_out("Creating new root entry.\n");
}else{
bool gotChanges = false;
rc = deck_check_changes(&d, &gotChanges);
if(rc) goto end;
else if(!gotChanges){
if(App.noChangesNoCry){
f_out("No changes were made. Exiting without an error.\n");
goto end;
}else{
rc = fcli_err_set(FSL_RC_NOOP,
"No file changes were made from "
"the parent version. Use --no-change-no-error "
"to permit a checkin anyway.");
goto end;
}
}
}
double const julian = true
? fsl_db_julian_now(fsl_cx_db_repo(f))
/* ^^^^ ms precision */
: fsl_julian_now() /* seconds precision */;
rc = fsl_deck_D_set(&d, julian);
if(rc) goto end;
rc = fsl_deck_save(&d, false);
if(rc) goto end;
f_out("Saved checkin %z (RID %"FSL_ID_T_PFMT")\n",
fsl_rid_to_uuid(d.f, d.rid), d.rid);
if(App.dryRun){
f_out("Dry-run mode. Rolling back.\n");
if(fcli_is_verbose()){
rc = fsl_deck_output(&d, NULL, NULL);
if(rc) goto end;
}
}else{
f_out("ACHTUNG: current checkouts of this repo will not "
"automatically know about this commit: they'll need "
"to 'update' to see the changes.\n");
}
assert(0==rc);
assert(fsl_cx_transaction_level(f)>0);
rc = fsl_cx_transaction_end(f, App.dryRun);
end:
fsl_free(dBranch);
fsl_buffer_clear(&fcontent);
fsl_deck_finalize(&d);
if(fsl_cx_transaction_level(f)){
fsl_cx_transaction_end(f, true);
}
return rc;
}
static void ciwoco_help(void){
f_out("Each non-flag argument is expected to be a file name "
"in the form LOCALNAME:REPONAME, e.g. '/etc/hosts:foo/hosts'. "
"The REPONAME part must be a repository-friendly name, e.g. no "
"leading slashes nor stray ./ components. The default value "
"for the REPONAME part is the LOCALNAME part, but that will "
"only be useful in limited cases. ");
f_out("As a special case, any leading './' on the REPONAME part "
"is elided\n");
}
int main(int argc, const char * const * argv ){
const fcli_cliflag FCliFlags[] = {
FCLI_FLAG("v", "version", "version", &App.sym,
"Version to derive from (default='trunk'). "
"Use the value '-' to create a new checkin "
"without a parent checkin. In that case, the "
"--branch flag is strongly recommended."),
FCLI_FLAG("b", "branch", "branch-name", &App.branch,
"Create a new branch for the new checkin."),
FCLI_FLAG("m", "comment", "string", &App.comment,
"Checkin comment."),
FCLI_FLAG("M", "comment-file", "filename", &App.commentFile,
"Checkin comment from a file (trumps -m)."),
FCLI_FLAG("p", "strip-prefix", "string", &App.stripPrefix,
"Any repository-side filenames which start with the "
"given prefix get that prefix stripped from them."),
FCLI_FLAG_BOOL("e","no-change-no-error", &App.noChangesNoCry,
"If no changes were made from the previous "
"version, exit without an error. New root entries "
"are always considered to be changes."),
FCLI_FLAG_BOOL("n", "dry-run", &App.dryRun,
"Dry-run mode. If verbose mode is active, "
"the resuling manifest is sent to stdout."),
FCLI_FLAG_BOOL_INVERT("i", "no-ignore-glob", &App.checkIgnoreGlobs,
"If set, do not honor the repo-level 'ignore-glob' "
"config setting to determine whether to skip a given "
"file."),
FCLI_FLAG_BOOL_INVERT(NULL, "no-r-card", &App.addRCard,
"If set, do not add an R-card to commits. "
"Even with this flag, an R-card is always "
"added for cases where not having one may "
"produce an ambiguous artifact."),
fcli_cliflag_empty_m // list MUST end with this (or equivalent)
};
const fcli_help_info FCliHelp = {
"Creates a checkin into a fossil repository without an "
"intermediary checkout.",
"file1:file1-repo-name ... fileN:file-N-repo-name",
// ^^^ very brief usage text, e.g. "file1 [...fileN]"
ciwoco_help // optional callback which outputs app-specific help
};
fsl_cx * f = NULL;
fcli.clientFlags.checkoutDir = NULL; // same effect as global --no-checkout flag.
int rc = fcli_setup_v2(argc, argv, FCliFlags, &FCliHelp);
if(rc) goto end;
else if((rc=fcli_has_unused_flags(false))) goto end;
f = fcli_cx();
if(!fsl_needs_repo(f)){
rc = FSL_RC_NOT_A_REPO;
goto end;
}
fsl_cx_flag_set(f, FSL_CX_F_CALC_R_CARD, App.addRCard);
if(App.stripPrefix) App.nStripPrefix = fsl_strlen(App.stripPrefix);
rc = do_everything();
end:
return fcli_end_of_main(rc);
}