Login
f-open.c at [d386e0d741]
Login

File f-apps/f-open.c artifact d08f8196be part of check-in d386e0d741


/* -*- 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);
}