/* -*- 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 implements the code to open and checkout a Fossil repository.
*/
#include "fossil-scm/fossil-cli.h" /* Fossil App mini-framework */
#include "fossil-scm/fossil-core.h"
#include "fossil-scm/fossil-db.h"
#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-util.h"
#include <errno.h>
#include <sys/stat.h>
#define MARKER(pfexp) \
do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \
printf pfexp; \
} while(0)
static int verbose = 0;
typedef struct {
int extracted;
int kept;
bool keep;
bool quiet;
} extract_state;
static int
fsl_repo_checkout_f_my(fsl_repo_checkout_state const *cState)
{
enum {
DATE_SHORT = 16
};
fsl_repo_extract_state const * xs = cState->xState;
fsl_cx *f = xs->f;
extract_state *state = (extract_state *)cState->callbackState;
static char tbuf[DATE_SHORT];
int rc = 0;
char mode = '?';
assert(f);
assert(xs->fc->uuid);
if(cState->fileWasWritten) {
mode = '+';
++state->extracted;
}else{
mode = '!';
++state->kept;
}
if(!state->quiet){
fsl_strftime_unix(tbuf, DATE_SHORT, "%d %b %Y", cState->mtime, 1);
f_out("[%c] %8"FSL_SIZE_T_PFMT" %s %s\n",
mode, cState->size, tbuf, xs->fc->name);
}
return rc;
}
int
main(int argc, char const * const *argv)
{
fsl_buffer buf = fsl_buffer_empty;
fsl_db *db = 0;
fsl_db *dbRepo = 0;
extract_state ex = {0,0,false};
fsl_cx *f = 0;
fsl_repo_open_checkout_opt opt = fsl_repo_open_checkout_opt_empty;
fsl_id_t rv = 0, prev_ckout = 0;
const char *repodir =0, *repository =0, *workdir = 0;
const char *sym = NULL;
char cwd[FILENAME_MAX];
bool empty = false, force = false, keep = false,
/*manifest = false,*/ mtime = false, nested = false, q = false,
uri = false;
int rc = 0;
fcli_cliflag const fcli_flags[] = {
FCLI_FLAG_BOOL("E", "empty", &empty,
"Initialise checkout as being empty. The checkout will be connected to "
"the local repository but no files will be written to disk."),
FCLI_FLAG_BOOL("f", "force", &force, "Continue opening the <repository> if "
"the working directory is not empty. Files present on disk with the same "
"name as files from the requested <version> being checked out will be "
"overwritten, other files will remain untouched."),
FCLI_FLAG_BOOL(0, "keep", &keep, "Only checkout files from the requested "
"<version> that do not have a file of the same name already present on "
"disk. Files with the same name as those from the requested <version> will"
" remain unmodified irrespective of whether their content is consistent "
"with that of the requested <version>. In such a case, the checkout will "
"immediately be in a changed state, which 'f-status' will report."),
#if 0
// Not implemented
FCLI_FLAG_BOOL(0, "manifest", &manifest,
"Only modify the manifest and manifest.uuid files."),
#endif
FCLI_FLAG_BOOL(0, "nested", &nested,
"Allow opening the <repository> inside an opened checkout."),
FCLI_FLAG_BOOL("q", "quiet", &q,
"Suppress non-error output unless --verbose is used."),
/*FCLI_FLAG(0, "repodir", "<dir>", &repodir,
"If <repository> is a URI that will be cloned, store the clone in <dir> "
"rather than the current working directory."),
*/
FCLI_FLAG_BOOL(0, "setmtime", &mtime,
"Set timestamps of all files to that "
"of the last check-in in which they were modified "
"(i.e., manifest time)."),
FCLI_FLAG(0, "workdir", "<dir>", &workdir,
"Open the checkout into <dir> instead of the current working directory. "
"This option will create <dir> if it does not already exist."),
fcli_cliflag_empty_m
};
fcli_help_info const fcli_help = {
"Open a new connection to the <repository>. A checkout of the most recent "
"check-in is created if no <version> is specified.",
"<repository> [<version>]",
NULL
};
fcli.checkoutDir = NULL; /* Cannot open yet; we're making it now. */
fcli.cliFlags = fcli_flags;
fcli.appHelp = &fcli_help;
rc = fcli_setup(argc, argv);
if(rc) goto end;
f = fcli_cx();
assert(!fsl_cx_db_repo(f));
verbose = fcli_is_verbose();
if(!verbose){
verbose = !q;
}
if(fcli_has_unused_flags(0)){
goto end;
}
dbRepo = fsl_cx_db_repo(f);
if(dbRepo){
// Was opened via -R flag.
repository = fsl_cx_db_file_repo(f, 0);
}else{
repository = fcli_next_arg(1);
if (!repository) {
rc = fcli_err_set(FSL_RC_MISUSE,
"Usage: %s [options] <repository> [<version>]\n"
"Or try --help", fcli.appName);
goto end;
}
if (fsl_str_glob("http://*", repository)
|| fsl_str_glob("https://*", repository)
|| fsl_str_glob("ssh:*", repository)
|| fsl_str_glob("file:*", repository)){
uri = true;
rc = fcli_err_set(FSL_RC_UNSUPPORTED,"URI-style names are "
"not currently supported.");
goto end;
}
}
assert(repository);
if(workdir && dbRepo){
MARKER(("FIXME: --workdir is currently incompatible with -R <REPO> "
"handling because of a mismatch in path translation.\n"));
}else if(workdir) {
char * zTmp = 0;
assert(!uri && "URI handling is still far away.");
if (!uri) {
rc = fsl_file_canonical_name(repository, &buf, 0);
if(rc) goto end;
zTmp = fsl_buffer_take(&buf);
fcli_fax(zTmp);
repository = zTmp;
}else if(repodir) {
assert(!"Not handled until URI support is added");
rc = fsl_file_canonical_name(repodir, &buf, 0);
if(rc) goto end;
zTmp = fsl_buffer_take(&buf);
fcli_fax(zTmp);
repodir = zTmp;
}
if(fsl_dir_check(workdir) < 1) {
rc = fsl_mkdir_for_file(workdir, 0);
if(rc) {
rc = fcli_err_set(rc, "Cannot create directory [%s].", workdir);
goto end;
}
}
if ((rc = fsl_chdir(workdir))) {
rc = fcli_err_set(rc, "Cannot chdir to [%s].",
workdir);
goto end;
}
}
if((rc = fsl_getcwd(cwd, FILENAME_MAX, NULL))){
rc = fcli_err_set(rc, "Rather unexpectedly cannot get the cwd!\n");
goto end;
}
if (!keep && !force) {
rc = fsl_dir_is_empty(cwd);
assert(rc>=0)/*"cannot" be <0 because fsl_getcwd() succeeded a
few nanoseconds ago.*/;
if(rc>0){
rc = fcli_err_set(FSL_RC_IO, "Directory [%s] is not empty\n"
"use the -f|--force option to override", cwd);
goto end;
}
}
if (!fsl_checkout_db_search(cwd, -1, !nested, &buf)) {
rc = fcli_err_set(FSL_RC_IO, "there is already an open tree at %s",
fsl_buffer_str(&buf));
goto end;
}
opt.targetDir = cwd;
opt.fileOverwritePolicy = keep ? FSL_OVERWRITE_NEVER : FSL_OVERWRITE_ALWAYS;
opt.dbOverwritePolicy = 1; /* 1 for new repo; 0 when already in checkout. */
if (uri) {
/*
* TODO: Implement clone and open when repository arg is a URI.
*/
}
if(rc) goto end;
if(dbRepo){
db = dbRepo;
}else{
FCLI_V(("Opening repository [%s]\n", repository));
rc = fsl_repo_open(f, repository);
if(rc) goto end;
db = dbRepo = fsl_cx_db_repo(f);
assert(db && "Can't be NULL if fsl_repo_open() succeeded, but..."
"TODO: add API which confirms that the db handle is-a repo "
"db in terms of schema.");
}
rc = fsl_cx_transaction_begin(f);
if(rc) goto end;
assert(!fsl_cx_db_checkout(f));
opt.ckoutDb = 0;
if(!empty){
if (!(sym = fcli_next_arg(1))){
sym = "tip";
}
}
if((rc = fsl_repo_open_checkout(f, &opt))){
goto end;
}
repository = fsl_cx_db_file_for_role(f, FSL_DBROLE_REPO, 0);
if(empty){
goto end;
}
if(!keep){
#if 0
// automatically done by fsl_vfile_changes_scan(). We may need a
// change to that API in order to handle the desired semantics for
// --keep.
fsl_db_exec(db, "DELETE FROM vfile");
#endif
prev_ckout = 0;
} else{
fsl_checkout_version_info(f, &prev_ckout, 0);
}
if(!sym){
char * verCheck = fsl_config_get_text(f, FSL_CONFDB_REPO,
"main-branch", 0);
if(!verCheck){
rc = fsl_sym_to_uuid(f, "tip", FSL_SATYPE_CHECKIN,
&verCheck, 0);
if(rc==FSL_RC_NOT_FOUND){
// No checkins in this repo.
fsl_cx_err_reset(f);
rc = 0;
empty = true;
sym = 0;
}else if(rc){
goto end;
}
}
if(verCheck){
fcli_fax(verCheck);
sym = verCheck;
}
}
assert(dbRepo && dbRepo->dbh);
if(!fsl_db_exists(dbRepo,"SELECT 1 FROM %s.event WHERE type='ci'",
fsl_db_role_label(FSL_DBROLE_REPO))){
// Assume there are no checkins in this repo, so nothing for us to do.
f_out("Repo contains no checkins, so there is nothing to check out.\n");
goto end;
}
rc = fsl_sym_to_rid(f, sym, FSL_SATYPE_CHECKIN, &rv);
if(rc){
// f's error state should contain a description of the problem.
if(FSL_RC_NOT_FOUND){
f_out("No checkout found for '%s'. This is normal if the repo "
"has no checkins, else it is an error.\n", sym);
}
goto end;
}
rc = fsl_vfile_load_from_rid(f, rv);
if(rc) goto end;
assert(f->ckout.rid==rv);
if(prev_ckout == rv){
keep = true; /* Why rewrite to disk? If user wants this they can use -f. */
}
/*
* TODO: Implement checkout.c::uncheckout() to unlink from disk and clear from
* the vfile table all prev_ckout files, and remove any resulting empty
* directories unless set in 'empty-dirs'. For now, this honours the caller's
* '--keep' option but leaves the workspace cluttered with any files already
* on disk that are unrelated to the requested checkout version when '--keep'
* is not passed.
*/
ex.keep = keep;
ex.extracted = 0;
ex.kept = 0;
ex.quiet = q;
fsl_repo_checkout_opt cOpt = fsl_repo_checkout_opt_empty;
cOpt.vid = rv;
cOpt.callbackState = &ex;
cOpt.callback = fsl_repo_checkout_f_my;
cOpt.fileOverwritePolicy = keep ? -1 : 1;
cOpt.setMtime = mtime;
rc = fsl_repo_checkout(f, &cOpt);
if(rc) goto end;
f_out("\nChecked out %d file(s) from %s [RID: %"FSL_ID_T_PFMT"] %z.\n",
ex.extracted, sym, rv, fsl_rid_to_uuid(f, rv));
if(ex.kept){
f_out("%d file(s) left unchanged on disk\n", ex.kept);
}
end:
if(fsl_cx_transaction_level(f)){
if(rc) fsl_cx_transaction_end(f, 1);
else{
rc = fsl_cx_transaction_end(f, 0);
rc = fsl_cx_uplift_db_error2(f, db, rc);
}
}
fsl_buffer_clear(&buf);
return fcli_end_of_main(rc);
}