libfossil

f-ciwoco.c
Login

f-ciwoco.c

File f-apps/f-ciwoco.c from the latest check-in


/* -*- 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 = {
.sym = "trunk",
.branch = NULL,
.comment = NULL,
.commentFile = NULL,
.stripPrefix = NULL,
.nStripPrefix = 0,
.addRCard = true,
.dryRun = false,
.checkIgnoreGlobs = true,
.noChangesNoCry = false
};

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