checkout.c at [dd8995bbdd]
Not logged in

File src/checkout.c artifact b14f9fb665 part of check-in dd8995bbdd


/*
** Copyright (c) 2007 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)

** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains code used to check-out versions of the project
** from the local repository.
*/
#include "config.h"
#include "checkout.h"
#include <assert.h>
#include <zlib.h>

/*
** Check to see if there is an existing check-out that has been
** modified.  Return values:
**
**     0:   There is an existing check-out but it is unmodified
**     1:   There is a modified check-out - there are unsaved changes
*/
int unsaved_changes(unsigned int cksigFlags){
  int vid;
  db_must_be_within_tree();
  vid = db_lget_int("checkout",0);
  vfile_check_signature(vid, cksigFlags|CKSIG_ENOTFILE);
  return db_exists("SELECT 1 FROM vfile WHERE chnged"
                   " OR coalesce(origname!=pathname,0)");
}

/*
** Undo the current check-out.  Unlink all files from the disk.
** Clear the VFILE table.
**
** Also delete any directory that becomes empty as a result of deleting
** files due to this operation, as long as that directory is not the
** current working directory and is not on the empty-dirs list.
*/
void uncheckout(int vid){
  char *zPwd;
  if( vid<=0 ) return;
  sqlite3_create_function(g.db, "dirname",1,SQLITE_UTF8,0,
                          file_dirname_sql_function, 0, 0);
  sqlite3_create_function(g.db, "unlink",1,SQLITE_UTF8|SQLITE_DIRECTONLY,0,
                          file_delete_sql_function, 0, 0);
  sqlite3_create_function(g.db, "rmdir", 1, SQLITE_UTF8|SQLITE_DIRECTONLY, 0,
                          file_rmdir_sql_function, 0, 0);
  db_multi_exec(
    "CREATE TEMP TABLE dir_to_delete(name TEXT %s PRIMARY KEY)WITHOUT ROWID",
    filename_collation()
  );
  db_multi_exec(
    "INSERT OR IGNORE INTO dir_to_delete(name)"
    "  SELECT dirname(pathname) FROM vfile"
    "   WHERE vid=%d AND mrid>0",
    vid
  );
  do{
    db_multi_exec(
      "INSERT OR IGNORE INTO dir_to_delete(name)"
      " SELECT dirname(name) FROM dir_to_delete;"
    );
  }while( db_changes() );
  db_multi_exec(
    "SELECT unlink(%Q||pathname) FROM vfile"
    " WHERE vid=%d AND mrid>0;",
    g.zLocalRoot, vid
  );
  ensure_empty_dirs_created(1);
  zPwd = file_getcwd(0,0);
  db_multi_exec(
    "SELECT rmdir(%Q||name) FROM dir_to_delete"
    " WHERE (%Q||name)<>%Q ORDER BY name DESC",
    g.zLocalRoot, g.zLocalRoot, zPwd
  );
  fossil_free(zPwd);
  db_multi_exec("DELETE FROM vfile WHERE vid=%d", vid);
}


/*
** Given the abbreviated hash of a version, load the content of that
** version in the VFILE table.  Return the VID for the version.
**
** If anything goes wrong, panic.
*/
int load_vfile(const char *zName, int forceMissingFlag){
  Blob uuid;
  int vid;

  blob_init(&uuid, zName, -1);
  if( name_to_uuid(&uuid, 1, "ci") ){
    fossil_fatal("%s", g.zErrMsg);
  }
  vid = db_int(0, "SELECT rid FROM blob WHERE uuid=%B", &uuid);
  if( vid==0 ){
    fossil_fatal("no such check-in: %s", g.argv[2]);
  }
  if( !is_a_version(vid) ){
    fossil_fatal("object [%S] is not a check-in", blob_str(&uuid));
  }
  if( load_vfile_from_rid(vid) && !forceMissingFlag ){
    fossil_fatal("missing content, unable to check out");
  };
  return vid;
}

/*
** Set or clear the vfile.isexe flag for a file.
*/
static void set_or_clear_isexe(const char *zFilename, int vid, int onoff){
  static Stmt s;
  db_static_prepare(&s,
    "UPDATE vfile SET isexe=:isexe"
    " WHERE vid=:vid AND pathname=:path AND isexe!=:isexe"
  );
  db_bind_int(&s, ":isexe", onoff);
  db_bind_int(&s, ":vid", vid);
  db_bind_text(&s, ":path", zFilename);
  db_step(&s);
  db_reset(&s);
}

/*
** Set or clear the execute permission bit (as appropriate) for all
** files in the current check-out, and replace files that have
** symlink bit with actual symlinks.
*/
void checkout_set_all_exe(int vid){
  Blob filename;
  int baseLen;
  Manifest *pManifest;
  ManifestFile *pFile;

  /* Check the EXE permission status of all files
  */
  pManifest = manifest_get(vid, CFTYPE_MANIFEST, 0);
  if( pManifest==0 ) return;
  blob_zero(&filename);
  blob_appendf(&filename, "%s", g.zLocalRoot);
  baseLen = blob_size(&filename);
  manifest_file_rewind(pManifest);
  while( (pFile = manifest_file_next(pManifest, 0))!=0 ){
    int isExe;
    blob_append(&filename, pFile->zName, -1);
    isExe = pFile->zPerm && strstr(pFile->zPerm, "x");
    file_setexe(blob_str(&filename), isExe);
    set_or_clear_isexe(pFile->zName, vid, isExe);
    blob_resize(&filename, baseLen);
  }
  blob_reset(&filename);
  manifest_destroy(pManifest);
}


/*
** If the "manifest" setting is true, then automatically generate
** files named "manifest" and "manifest.uuid" containing, respectively,
** the text of the manifest and the artifact ID of the manifest.
** If the manifest setting is set, but is not a boolean value, then treat
** each character as a flag to enable writing "manifest", "manifest.uuid" or
** "manifest.tags".
*/
void manifest_to_disk(int vid){
  char *zManFile;
  int flg;

  flg = db_get_manifest_setting(0);

  if( flg & MFESTFLG_RAW ){
    Blob manifest = BLOB_INITIALIZER;
    content_get(vid, &manifest);
    sterilize_manifest(&manifest, CFTYPE_MANIFEST);
    zManFile = mprintf("%smanifest", g.zLocalRoot);
    blob_write_to_file(&manifest, zManFile);
    free(zManFile);
    blob_reset(&manifest);
  }else{
    if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest'") ){
      zManFile = mprintf("%smanifest", g.zLocalRoot);
      file_delete(zManFile);
      free(zManFile);
    }
  }
  if( flg & MFESTFLG_UUID ){
    Blob hash;
    zManFile = mprintf("%smanifest.uuid", g.zLocalRoot);
    blob_set_dynamic(&hash, rid_to_uuid(vid));
    blob_append(&hash, "\n", 1);
    blob_write_to_file(&hash, zManFile);
    free(zManFile);
    blob_reset(&hash);
  }else{
    if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest.uuid'") ){
      zManFile = mprintf("%smanifest.uuid", g.zLocalRoot);
      file_delete(zManFile);
      free(zManFile);
    }
  }
  if( flg & MFESTFLG_TAGS ){
    Blob taglist = BLOB_INITIALIZER;
    zManFile = mprintf("%smanifest.tags", g.zLocalRoot);
    get_checkin_taglist(vid, &taglist);
    blob_write_to_file(&taglist, zManFile);
    free(zManFile);
    blob_reset(&taglist);
  }else{
    if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest.tags'") ){
      zManFile = mprintf("%smanifest.tags", g.zLocalRoot);
      file_delete(zManFile);
      free(zManFile);
    }
  }
}

/*
** Find the branch name and all symbolic tags for a particular check-in
** identified by "rid".
**
** The branch name is actually only extracted if this procedure is run
** from within a local check-out.  And the branch name is not the branch
** name for "rid" but rather the branch name for the current check-out.
** It is unclear if the rid parameter is always the same as the current
** check-out.
*/
void get_checkin_taglist(int rid, Blob *pOut){
  Stmt stmt;
  char *zCurrent;
  blob_reset(pOut);
  zCurrent = db_text(0, "SELECT value FROM tagxref"
                        " WHERE rid=%d AND tagid=%d", rid, TAG_BRANCH);
  blob_appendf(pOut, "branch %s\n", zCurrent);
  db_prepare(&stmt, "SELECT substr(tagname, 5)"
                    "  FROM tagxref, tag"
                    " WHERE tagxref.rid=%d"
                    "   AND tagxref.tagtype>0"
                    "   AND tag.tagid=tagxref.tagid"
                    "   AND tag.tagname GLOB 'sym-*'", rid);
  while( db_step(&stmt)==SQLITE_ROW ){
    const char *zName;
    zName = db_column_text(&stmt, 0);
    blob_appendf(pOut, "tag %s\n", zName);
  }
  db_reset(&stmt);
  db_finalize(&stmt);
}


/*
** COMMAND: checkout*
** COMMAND: co#
**
** Usage: %fossil checkout ?VERSION | --latest? ?OPTIONS?
**    or: %fossil co ?VERSION | --latest? ?OPTIONS?
**
** NOTE: Most people use "fossil update" instead of "fossil checkout" for
** day-to-day operations.  If you are new to Fossil and trying to learn your
** way around, it is recommended that you become familiar with the
** "fossil update" command first.
**
** This command changes the current check-out to the version specified
** as an argument.  The command aborts if there are edited files in the
** current check-out unless the --force option is used.  The --keep option
** leaves files on disk unchanged, except the manifest and manifest.uuid
** files.
**
** The --latest flag can be used in place of VERSION to check-out the
** latest version in the repository.
**
** Options:
**    -f|--force        Ignore edited files in the current check-out
**    -k|--keep         Only update the manifest file(s)
**    --force-missing   Force check-out even if content is missing
**    --prompt          Prompt before overwriting when --force is used
**    --setmtime        Set timestamps of all files to match their SCM-side
**                      times (the timestamp of the last check-in which modified
**                      them)
**
** See also: [[update]]
*/
void checkout_cmd(void){
  int forceFlag;                 /* Force check-out even if edits exist */
  int forceMissingFlag;          /* Force check-out even if missing content */
  int keepFlag;                  /* Do not change any files on disk */
  int latestFlag;                /* Check out the latest version */
  char *zVers;                   /* Version to check out */
  int promptFlag;                /* True to prompt before overwriting */
  int vid, prior;
  int setmtimeFlag;              /* --setmtime.  Set mtimes on files */
  Blob cksum1, cksum1b, cksum2;

  db_must_be_within_tree();
  db_begin_transaction();
  forceMissingFlag = find_option("force-missing",0,0)!=0;
  keepFlag = find_option("keep","k",0)!=0;
  forceFlag = find_option("force","f",0)!=0;
  latestFlag = find_option("latest",0,0)!=0;
  promptFlag = find_option("prompt",0,0)!=0 || forceFlag==0;
  setmtimeFlag = find_option("setmtime",0,0)!=0;

  if( keepFlag != 0 ){
    /* After flag collection, in order not to affect promptFlag */
    forceFlag=1;
  }

  /* We should be done with options.. */
  verify_all_options();

  if( (latestFlag!=0 && g.argc!=2) || (latestFlag==0 && g.argc!=3) ){
     usage("VERSION|--latest ?--force? ?--keep?");
  }
  if( !forceFlag && unsaved_changes(0) ){
    fossil_fatal("there are unsaved changes in the current check-out");
  }
  if( forceFlag ){
    db_multi_exec("DELETE FROM vfile");
    prior = 0;
  }else{
    prior = db_lget_int("checkout",0);
  }
  if( latestFlag ){
    compute_leaves(db_lget_int("checkout",0), 1);
    zVers = db_text(0, "SELECT uuid FROM leaves, event, blob"
                       " WHERE event.objid=leaves.rid AND blob.rid=leaves.rid"
                       " ORDER BY event.mtime DESC");
    if( zVers==0 ){
      zVers = db_text(0, "SELECT uuid FROM event, blob"
                         " WHERE event.objid=blob.rid AND event.type='ci'"
                         " ORDER BY event.mtime DESC");
    }
    if( zVers==0 ){
      db_end_transaction(0);
      return;
    }
  }else{
    zVers = g.argv[2];
  }
  vid = load_vfile(zVers, forceMissingFlag);
  if( prior==vid ){
    if( setmtimeFlag ) vfile_check_signature(vid, CKSIG_SETMTIME);
    db_end_transaction(0);
    return;
  }
  if( !keepFlag ){
    uncheckout(prior);
  }
  db_multi_exec("DELETE FROM vfile WHERE vid!=%d", vid);
  if( !keepFlag ){
    vfile_to_disk(vid, 0, !g.fQuiet, promptFlag);
  }
  checkout_set_all_exe(vid);
  manifest_to_disk(vid);
  ensure_empty_dirs_created(0);
  db_set_checkout(vid);
  undo_reset();
  db_multi_exec("DELETE FROM vmerge");
  if( !keepFlag && db_get_boolean("repo-cksum",1) ){
    vfile_aggregate_checksum_manifest(vid, &cksum1, &cksum1b);
    vfile_aggregate_checksum_disk(vid, &cksum2);
    if( blob_compare(&cksum1, &cksum2) ){
      fossil_print("WARNING: manifest checksum does not agree with disk\n");
    }
    if( blob_size(&cksum1b) && blob_compare(&cksum1, &cksum1b) ){
      fossil_print("WARNING: manifest checksum does not agree with manifest\n");
    }
  }
  if( setmtimeFlag ) vfile_check_signature(vid, CKSIG_SETMTIME);
  db_end_transaction(0);
}

/*
** Unlink the local database file
*/
static void unlink_local_database(int manifestOnly){
  const char *zReserved;
  int i;
  for(i=0; (zReserved = fossil_reserved_name(i, 1))!=0; i++){
    if( manifestOnly==0 || zReserved[0]=='m' ){
      char *z;
      z = mprintf("%s%s", g.zLocalRoot, zReserved);
      file_delete(z);
      free(z);
    }
  }
}

/*
** COMMAND: close*
**
** Usage: %fossil close ?OPTIONS?
**
** The opposite of "[[open]]".  Close the current database connection.
** Require a -f or --force flag if there are unsaved changes in the
** current check-out or if there is non-empty stash.
**
** Options:
**   -f|--force  Necessary to close a check-out with uncommitted changes
**
** See also: [[open]]
*/
void close_cmd(void){
  int forceFlag = find_option("force","f",0)!=0;
  db_must_be_within_tree();

  /* We should be done with options.. */
  verify_all_options();

  if( !forceFlag && unsaved_changes(0) ){
    fossil_fatal("there are unsaved changes in the current check-out");
  }
  if( !forceFlag
   && db_table_exists("localdb","stash")
   && db_exists("SELECT 1 FROM localdb.stash")
  ){
    fossil_fatal("closing the check-out will delete your stash");
  }
  if( db_is_writeable("repository") ){
    db_unset_mprintf(1, "ckout:%q", g.zLocalRoot);
  }
  unlink_local_database(1);
  db_close(1);
  unlink_local_database(0);
}


/*
** COMMAND: get
**
** Usage: %fossil get URL ?VERSION? ?OPTIONS?
**
** Download a single check-in from a remote repository named URL and
** unpack all of the files locally. The check-in is identified by VERSION.
**
** URL can be a traditional URL like one of:
**
**    *   https://domain.com/project
**    *   ssh://my-server/project.fossil
**    *   file:/home/user/Fossils/project.fossil
**
** Or URL can be just the name of a local repository without the "file:"
** prefix.
**
** This command works by downloading an SQL archive of the requested
** check-in and then extracting all the files from the archive.
**
** Options:
**   --dest DIRECTORY         Extract files into DIRECTORY.  Use "--dest ."
**                            to extract into the local directory.
**
**   -f|--force               Overwrite existing files
**
**   --list                   List all the files that would have been checked
**                            out but do not actually write anything to the
**                            filesystem.
**
**   --sqlar ARCHIVE          Store the check-out in an SQL-archive rather
**                            than unpacking them into separate files.
**
**   -v|--verbose             Show all files as they are extracted
*/
void get_cmd(void){
  int forceFlag = find_option("force","f",0)!=0;
  int bVerbose = find_option("verbose","v",0)!=0;
  int bQuiet = find_option("quiet","q",0)!=0;
  int bDebug = find_option("debug",0,0)!=0;
  int bList = find_option("list",0,0)!=0;
  const char *zSqlArchive = find_option("sqlar",0,1);
  const char *z;
  char *zDest = 0;        /* Where to store results */
  char *zSql;             /* SQL used to query the results */
  const char *zUrl;       /* Url to get */
  const char *zVers;      /* Version name to get */
  unsigned int mHttpFlags = HTTP_GENERIC|HTTP_NOCOMPRESS;
  Blob in, out;           /* I/O for the HTTP request */
  Blob file;              /* A file to extract */
  sqlite3 *db;            /* Database containing downloaded sqlar */
  sqlite3_stmt *pStmt;    /* Statement for querying the database */
  int rc;                 /* Result of subroutine calls */
  int nFile = 0;          /* Number of files written */
  int nDir = 0;           /* Number of directories written */
  i64 nByte = 0;          /* Number of bytes written */

  z = find_option("dest",0,1);
  if( z ) zDest = fossil_strdup(z);
  verify_all_options();
  if( g.argc<3 || g.argc>4 ){
    usage("URL ?VERSION? ?OPTIONS?");
  }
  zUrl = g.argv[2];
  zVers = g.argc==4 ? g.argv[3] : db_get("main-branch", 0);

  /* Parse the URL of the repository */
  url_parse(zUrl, 0);

  /* Construct an appropriate name for the destination directory */
  if( zDest==0 ){
    int i;
    const char *zTail;
    const char *zDot;
    int n;
    if( g.url.isFile ){
      zTail = file_tail(g.url.name);
    }else{
      zTail = file_tail(g.url.path);
    }
    zDot = strchr(zTail,'.');
    if( zDot==0 ) zDot = zTail+strlen(zTail);
    n = (int)(zDot - zTail);
    zDest = mprintf("%.*s-%s", n, zTail, zVers);
    for(i=0; zDest[i]; i++){
      char c = zDest[i];
      if( !fossil_isalnum(c) && c!='-' && c!='^' && c!='~' && c!='_' ){
        zDest[i] = '-';
      }
    }
  }
  if( bDebug ){
    fossil_print("dest            = %s\n", zDest);
  }

  /* Error checking */
  if( zDest!=file_tail(zDest) ){
    fossil_fatal("--dest must be a simple directory name, not a path");
  }
  if( zVers!=file_tail(zVers) ){
    fossil_fatal("The \"fossil get\" command does not currently work with"
                 " version names that contain \"/\". This will be fixed in"
                 " a future release.");
  }
  /* To relax the restrictions above, change the subpath URL formula below
  ** to use query parameters.  Ex:  /sqlar?r=%t&name=%t */

  if( !forceFlag ){
    if( zSqlArchive ){
      if( file_isdir(zSqlArchive, ExtFILE)>0 ){
        fossil_fatal("file already exists: \"%s\"", zSqlArchive);
      }
    }else if( file_isdir(zDest, ExtFILE)>0 ){
      if( fossil_strcmp(zDest,".")==0 ){
        if( file_directory_size(zDest,0,1) ){
          fossil_fatal("current directory is not empty");
        }
      }else{
        fossil_fatal("\"%s\" already exists", zDest);
      }
    }
  }

  /* Construct a subpath on the URL if necessary */
  if( g.url.isFile ){
    g.url.subpath = mprintf("/sqlar/%t/%t.sqlar", zVers, zDest);
  }else{
    g.url.subpath = mprintf("%s/sqlar/%t/%t.sqlar", g.url.path, zVers, zDest);
  }

  if( bDebug ){
    urlparse_print(0);
  }

  /* Fetch the ZIP archive for the requested check-in */
  blob_init(&in, 0, 0);
  blob_init(&out, 0, 0);
  if( bDebug ) mHttpFlags |= HTTP_VERBOSE;
  if( bQuiet ) mHttpFlags |= HTTP_QUIET;
  rc = http_exchange(&in, &out, mHttpFlags, 4, 0);
  if( rc 
   || out.nUsed<512
   || (out.nUsed%512)!=0
   || memcmp(out.aData,"SQLite format 3",16)!=0
  ){
    fossil_fatal("Server did not return the requested check-in.");
  }

  if( zSqlArchive ){
    blob_write_to_file(&out, zSqlArchive);
    if( bVerbose ) fossil_print("%s\n", zSqlArchive);
    return;
  }

  rc = sqlite3_open(":memory:", &db);
  if( rc==SQLITE_OK ){
    int sz = blob_size(&out);
    rc = sqlite3_deserialize(db, 0, (unsigned char*)blob_buffer(&out), sz, sz,
                             SQLITE_DESERIALIZE_READONLY);
  }
  if( rc!=SQLITE_OK ){
    fossil_fatal("Cannot create an in-memory database: %s",
                 sqlite3_errmsg(db));
  }
  zSql = mprintf("SELECT name, mode, sz, data FROM sqlar"
                 " WHERE name GLOB '%q*'", zDest);
  rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0);
  fossil_free(zSql);
  if( rc!=0 ){
    fossil_fatal("SQL error: %s\n", sqlite3_errmsg(db));
  }
  blob_init(&file, 0, 0);
  while( sqlite3_step(pStmt)==SQLITE_ROW ){
    const char *zFilename = (const char*)sqlite3_column_text(pStmt, 0);
    int mode = sqlite3_column_int(pStmt, 1);
    int sz = sqlite3_column_int(pStmt, 2);
    if( bList ){
      fossil_print("%s\n", zFilename);
    }else  if( mode & 0x4000 ){
      /* A directory name */
      nDir++;
      file_mkdir(zFilename, ExtFILE, 1);
    }else{
      /* A file */
      unsigned char *inBuf = (unsigned char*)sqlite3_column_blob(pStmt,3);
      unsigned int nIn = (unsigned int)sqlite3_column_bytes(pStmt,3);
      unsigned long int nOut2 = (unsigned long int)sz;
      nFile++;
      nByte += sz;
      blob_resize(&file, sz);
      if( nIn<sz ){
        rc = uncompress((unsigned char*)blob_buffer(&file), &nOut2,
                        inBuf, nIn);
        if( rc!=Z_OK ){
          fossil_fatal("Failed to uncompress file %s", zFilename);
        }
      }else{
        memcpy(blob_buffer(&file), inBuf, sz);
      }
      blob_write_to_file(&file, zFilename);
      if( mode & 0x40 ){
        file_setexe(zFilename, 1);
      }
      blob_zero(&file);
      if( bVerbose ){
        fossil_print("%s\n", zFilename);
      }
    }
  }
  sqlite3_finalize(pStmt);
  sqlite3_close(db);
  blob_zero(&out);
  if( !bVerbose && !bQuiet && nFile>0 && zDest ){
    fossil_print("%d files (%,lld bytes) written into %s",
                 nFile, nByte, zDest);
    if( nDir>1 ){
      fossil_print(" and %d subdirectories\n", nDir-1);
    }else{
      fossil_print("\n");
    }
  }
}