/* -*- 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 #include #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->fCard->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->fCard->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 if " "the working directory is not empty. Files present on disk with the same " "name as files from the requested being checked out will be " "overwritten, other files will remain untouched."), FCLI_FLAG_BOOL(0, "keep", &keep, "Only checkout files from the requested " " that do not have a file of the same name already present on " "disk. Files with the same name as those from the requested will" " remain unmodified irrespective of whether their content is consistent " "with that of the requested . 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 inside an opened checkout."), FCLI_FLAG_BOOL("q", "quiet", &q, "Suppress non-error output unless --verbose is used."), /*FCLI_FLAG(0, "repodir", "", &repodir, "If is a URI that will be cloned, store the clone in " "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", "", &workdir, "Open the checkout into instead of the current working directory. " "This option will create if it does not already exist."), fcli_cliflag_empty_m }; fcli_help_info const fcli_help = { "Open a new connection to the . A checkout of the most recent " "check-in is created if no is specified.", " []", 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] []\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 " "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.checkinRid = 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(f && 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); }