Fossil

Artifact [ebcb9bb2]
Login

Artifact [ebcb9bb2]

Artifact ebcb9bb2def77273c8511ad039d129c4c735c3869e28bc667b7d333c44f8b1e8:


/*
** Copyright (c) 2010 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@sqlite.org
**
*******************************************************************************
**
** This file contains code used to implement the "stash" command.
*/
#include "config.h"
#include "stash.h"
#include <assert.h>


/*
** SQL code to implement the tables needed by the stash.
**
** Historical schema changes:
**
**   2019-01-19:   stash.hash and stashfile.hash columns added.  The
**                 corresponding stash.vid and stashfile.rid columns are
**                 retained for compatibility with older versions of
**                 fossil but are no longer used.
**
**   2016-10-16:   Change the PRIMARY KEY on stashfile from (origname,stashid)
**                 to (newname,stashid).
**
**   2011-09-01:   stashfile.isLink column added
**
*/
static const char zStashInit[] =
@ CREATE TABLE IF NOT EXISTS localdb.stash(
@   stashid INTEGER PRIMARY KEY,     -- Unique stash identifier
@   vid INTEGER,                     -- Legacy baseline RID value. Do not use.
@   hash TEXT,                       -- The SHA hash for the baseline
@   comment TEXT,                    -- Comment for this stash.  Or NULL
@   ctime TIMESTAMP                  -- When the stash was created
@ );
@ CREATE TABLE IF NOT EXISTS localdb.stashfile(
@   stashid INTEGER REFERENCES stash,  -- Stash that contains this file
@   isAdded BOOLEAN,                   -- True if this is an added file
@   isRemoved BOOLEAN,                 -- True if this file is deleted
@   isExec BOOLEAN,                    -- True if file is executable
@   isLink BOOLEAN,                    -- True if file is a symlink
@   rid INTEGER,                       -- Legacy baseline RID value. Do not use
@   hash TEXT,                         -- Hash for baseline or NULL
@   origname TEXT,                     -- Original filename
@   newname TEXT,                      -- New name for file at next check-in
@   delta BLOB,                        -- Delta from baseline or raw content
@   PRIMARY KEY(newname, stashid)
@ );
@ INSERT OR IGNORE INTO vvar(name, value) VALUES('stash-next', 1);
;

/*
** Make sure the stash and stashfile tables exist and have been
** upgraded to their latest format.  Create and upgrade the tables
** as necessary.
*/
static void stash_tables_exist_and_current(void){
  if( db_table_has_column("localdb","stashfile","hash") ){
    /* The schema is up-to-date.  But it could be that an older version
    ** of Fossil that does no know about the stash.hash and stashfile.hash
    ** columns has run since the schema was updated, and added entries that
    ** have NULL hash columns.  Check for this case, and fill in any missing
    ** hash values.
    */
    if( db_int(0, "SELECT hash IS NULL FROM stash"
                  " ORDER BY stashid DESC LIMIT 1")
    ){
      db_multi_exec(
        "UPDATE stash"
        "   SET hash=(SELECT uuid FROM blob WHERE blob.rid=stash.vid)"
        " WHERE hash IS NULL;"
        "UPDATE stashfile"
        "   SET hash=(SELECT uuid FROM blob WHERE blob.rid=stashfile.rid)"
        " WHERE hash IS NULL AND rid>0;"
      );
    }
    return;
  }

  if( !db_table_exists("localdb","stashfile")
   || !db_table_exists("localdb","stash")
  ){
    /* Tables do not exist.  Create them from scratch. */
    db_multi_exec("DROP TABLE IF EXISTS localdb.stash;");
    db_multi_exec("DROP TABLE IF EXISTS localdb.stashfile;");
    db_multi_exec(zStashInit /*works-like:""*/);
    return;
  }

  /* The tables exists but are not necessarily current.  Upgrade them
  ** to the latest format.
  **
  ** We can assume the 2011-09-01 format that includes the stashfile.isLink
  ** column.  The only upgrades we need to worry about the PRIMARY KEY
  ** change on 2016-10-16 and the addition of the "hash" columns on
  ** 2019-01-19.
  */
  db_multi_exec(
    "ALTER TABLE localdb.stash RENAME TO old_stash;"
    "ALTER TABLE localdb.stashfile RENAME TO old_stashfile;"
  );
  db_multi_exec(zStashInit /*works-like:""*/);
  db_multi_exec(
    "INSERT INTO localdb.stash(stashid,vid,hash,comment,ctime)"
    " SELECT stashid, vid,"
    "   (SELECT uuid FROM blob WHERE blob.rid=old_stash.vid),"
    "   comment, ctime FROM old_stash;"
    "DROP TABLE old_stash;"
  );
  db_multi_exec(
    "INSERT INTO localdb.stashfile(stashid,isAdded,isRemoved,isExec,"
                                  "isLink,rid,hash,origname,newname,delta)"
    " SELECT stashid, isAdded, isRemoved, isExec, isLink, rid,"
    "   (SELECT uuid FROM blob WHERE blob.rid=old_stashfile.rid),"
    "   origname, newname, delta FROM old_stashfile;"
    "DROP TABLE old_stashfile;"
  );
}

/*
** Update the stash.vid and stashfile.rid values after a RID renumbering
** event.
*/
void stash_rid_renumbering_event(void){
  if( !db_table_has_column("localdb","stash","hash") ){
    /* If the stash schema was the older style that lacked hash value, then
    ** recovery is not possible.  Save off the old data, then reset the stash
    ** to empty. */
    if( db_table_exists("localdb","stash") ){
      db_multi_exec("ALTER TABLE stash RENAME TO broken_stash;");
      fossil_print("Unrecoverable stash content stored in \"broken_stash\"\n");
    }
    if( db_table_exists("localdb","stashfile") ){
      db_multi_exec("ALTER TABLE stashfile RENAME TO broken_stashfile;");
      fossil_print("Unrecoverable stashfile content stored"
                   " in \"broken_stashfile\"\n");
    }
  }else{
    /* Reset stash.vid and stash.rid values based on hashes */
    db_multi_exec(
      "UPDATE stash"
      "   SET vid=(SELECT rid FROM blob WHERE blob.uuid=stash.hash);"
      "UPDATE stashfile"
      "   SET rid=(SELECT rid FROM blob WHERE blob.uuid=stashfile.hash)"
      " WHERE hash IS NOT NULL;"
    );
  }
}

/*
** Add zFName to the stash given by stashid.  zFName might be the name of a
** file or a directory.  If a directory, add all changed files contained
** within that directory.
*/
static void stash_add_file_or_dir(int stashid, int vid, const char *zFName){
  char *zFile;          /* Normalized filename */
  char *zTreename;      /* Name of the file in the tree */
  Blob fname;           /* Filename relative to root */
  Blob sql;             /* Query statement text */
  Stmt q;               /* Query against the vfile table */
  Stmt ins;             /* Insert statement */

  zFile = mprintf("%/", zFName);
  file_tree_name(zFile, &fname, 0, 1);
  zTreename = blob_str(&fname);
  blob_zero(&sql);
  blob_append_sql(&sql,
    "SELECT deleted, isexe, islink, mrid, pathname, coalesce(origname,pathname)"
    "  FROM vfile"
    " WHERE vid=%d AND (chnged OR deleted OR origname NOT NULL OR mrid==0)",
    vid
  );
  if( fossil_strcmp(zTreename,".")!=0 ){
    blob_append_sql(&sql,
      "   AND (pathname GLOB '%q/*' OR origname GLOB '%q/*'"
            "  OR pathname=%Q OR origname=%Q)",
      zTreename, zTreename, zTreename, zTreename
    );
  }
  db_prepare(&q, "%s", blob_sql_text(&sql));
  blob_reset(&sql);
  db_prepare(&ins,
     "INSERT INTO stashfile(stashid, isAdded, isRemoved, isExec, isLink, rid, "
                           "hash, origname, newname, delta)"
     "VALUES(%d,:isadd,:isrm,:isexe,:islink,:rid,"
     "(SELECT uuid FROM blob WHERE rid=:rid),:orig,:new,:content)",
     stashid
  );
  while( db_step(&q)==SQLITE_ROW ){
    int deleted = db_column_int(&q, 0);
    int rid = db_column_int(&q, 3);
    const char *zName = db_column_text(&q, 4);
    const char *zOrig = db_column_text(&q, 5);
    char *zPath = mprintf("%s%s", g.zLocalRoot, zName);
    Blob content;

    db_bind_int(&ins, ":rid", rid);
    db_bind_int(&ins, ":isadd", rid==0);
    db_bind_int(&ins, ":isrm", deleted);
    db_bind_int(&ins, ":isexe", db_column_int(&q, 1));
    db_bind_int(&ins, ":islink", db_column_int(&q, 2));
    db_bind_text(&ins, ":orig", zOrig);
    db_bind_text(&ins, ":new", zName);

    if( rid==0 ){
      /* A new file */
      blob_read_from_file(&content, zPath, RepoFILE);
      db_bind_blob(&ins, ":content", &content);
    }else if( deleted ){
      blob_zero(&content);
      db_bind_null(&ins, ":content");
    }else{
      /* A modified file */
      Blob orig;
      Blob disk;

      blob_read_from_file(&disk, zPath, RepoFILE);
      content_get(rid, &orig);
      blob_delta_create(&orig, &disk, &content);
      blob_reset(&orig);
      blob_reset(&disk);
      db_bind_blob(&ins, ":content", &content);
    }
    db_bind_int(&ins, ":islink", file_islink(zPath));
    db_step(&ins);
    db_reset(&ins);
    fossil_free(zPath);
    blob_reset(&content);
  }
  db_finalize(&ins);
  db_finalize(&q);
  fossil_free(zFile);
  blob_reset(&fname);
}

/*
** Create a new stash based on the uncommitted changes currently in
** the working directory.
**
** If the "-m" or "--comment" command-line option is present, gather
** its argument as the stash comment.
**
** If files are named on the command-line, then only stash the named
** files.
*/
static int stash_create(void){
  const char *zComment;              /* Comment to add to the stash */
  int stashid;                       /* ID of the new stash */
  int vid;                           /* Current checkout */

  zComment = find_option("comment", "m", 1);
  verify_all_options();
  if( zComment==0 ){
    Blob prompt;                       /* Prompt for stash comment */
    Blob comment;                      /* User comment reply */
#if defined(_WIN32) || defined(__CYGWIN__)
    int bomSize;
    const unsigned char *bom = get_utf8_bom(&bomSize);
    blob_init(&prompt, (const char *) bom, bomSize);
#else
    blob_zero(&prompt);
#endif
    blob_append(&prompt,
       "\n"
       "# Enter a description of what is being stashed.  Lines beginning\n"
       "# with \"#\" are ignored.  Stash comments are plain text except\n"
       "# newlines are not preserved.\n",
       -1);
    prompt_for_user_comment(&comment, &prompt);
    blob_reset(&prompt);
    zComment = blob_str(&comment);
  }
  stashid = db_lget_int("stash-next", 1);
  db_lset_int("stash-next", stashid+1);
  vid = db_lget_int("checkout", 0);
  vfile_check_signature(vid, 0);
  db_multi_exec(
    "INSERT INTO stash(stashid,vid,hash,comment,ctime)"
    "VALUES(%d,%d,(SELECT uuid FROM blob WHERE rid=%d),%Q,julianday('now'))",
    stashid, vid, vid, zComment
  );
  if( g.argc>3 ){
    int i;
    for(i=3; i<g.argc; i++){
      stash_add_file_or_dir(stashid, vid, g.argv[i]);
    }
  }else{
    stash_add_file_or_dir(stashid, vid, g.zLocalRoot);
  }
  return stashid;
}

/*
** Apply a stash to the current checkout.
*/
static void stash_apply(int stashid, int nConflict){
  int vid;
  Stmt q;
  db_prepare(&q,
     "SELECT blob.rid, isRemoved, isExec, isLink, origname, newname, delta"
     "  FROM stashfile, blob WHERE stashid=%d AND blob.uuid=stashfile.hash"
     " UNION ALL SELECT 0, isRemoved, isExec, isLink, origname, newname, delta"
     "  FROM stashfile WHERE stashid=%d AND stashfile.hash IS NULL",
     stashid, stashid
  );
  vid = db_lget_int("checkout",0);
  db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s)",
                filename_collation());
  while( db_step(&q)==SQLITE_ROW ){
    int rid = db_column_int(&q, 0);
    int isRemoved = db_column_int(&q, 1);
    int isExec = db_column_int(&q, 2);
    int isLink = db_column_int(&q, 3);
    const char *zOrig = db_column_text(&q, 4);
    const char *zNew = db_column_text(&q, 5);
    char *zOPath = mprintf("%s%s", g.zLocalRoot, zOrig);
    char *zNPath = mprintf("%s%s", g.zLocalRoot, zNew);
    Blob delta;
    undo_save(zNew);
    blob_zero(&delta);
    if( rid==0 ){
      db_multi_exec("INSERT OR IGNORE INTO sfile(pathname) VALUES(%Q)", zNew);
      db_ephemeral_blob(&q, 6, &delta);
      blob_write_to_file(&delta, zNPath);
      file_setexe(zNPath, isExec);
    }else if( isRemoved ){
      fossil_print("DELETE %s\n", zOrig);
      file_delete(zOPath);
    }else if( file_unsafe_in_tree_path(zNPath) ){
      /* Ignore the unsafe path */
    }else{
      Blob a, b, out, disk;
      int isNewLink = file_islink(zOPath);
      db_ephemeral_blob(&q, 6, &delta);
      blob_read_from_file(&disk, zOPath, RepoFILE);
      content_get(rid, &a);
      blob_delta_apply(&a, &delta, &b);
      if( isLink == isNewLink && blob_compare(&disk, &a)==0 ){
        if( isLink || isNewLink ){
          file_delete(zNPath);
        }
        if( isLink ){
          symlink_create(blob_str(&b), zNPath);
        }else{
          blob_write_to_file(&b, zNPath);
        }
        file_setexe(zNPath, isExec);
        fossil_print("UPDATE %s\n", zNew);
      }else{
        int rc;
        if( isLink || isNewLink ){
          rc = -1;
          blob_zero(&b); /* because we reset it later */
          fossil_print("***** Cannot merge symlink %s\n", zNew);
        }else{
          rc = merge_3way(&a, zOPath, &b, &out, MERGE_KEEP_FILES);
          blob_write_to_file(&out, zNPath);
          blob_reset(&out);
          file_setexe(zNPath, isExec);
        }
        if( rc ){
          fossil_print("CONFLICT %s\n", zNew);
          nConflict++;
        }else{
          fossil_print("MERGE %s\n", zNew);
        }
      }
      blob_reset(&a);
      blob_reset(&b);
      blob_reset(&disk);
    }
    blob_reset(&delta);
    if( fossil_strcmp(zOrig,zNew)!=0 ){
      undo_save(zOrig);
      file_delete(zOPath);
      db_multi_exec(
        "UPDATE vfile SET pathname='%q', origname='%q'"
        " WHERE pathname='%q' %s AND vid=%d",
        zNew, zOrig, zOrig, filename_collation(), vid
      );
    }
  }
  stash_add_files_in_sfile(vid);
  db_finalize(&q);
  if( nConflict ){
    fossil_print(
      "WARNING: %d merge conflicts - see messages above for details.\n",
      nConflict);
  }
}

/*
** Show the diffs associate with a single stash.
*/
static void stash_diff(
  int stashid,             /* The stash entry to diff */
  int fBaseline,           /* Diff against original baseline check-in if true */
  DiffConfig *pCfg         /* Diff formatting options */
){
  Stmt q;
  Blob empty;
  int bWebpage = (pCfg->diffFlags & (DIFF_WEBPAGE|DIFF_JSON|DIFF_TCL))!=0;
  blob_zero(&empty);
  diff_begin(pCfg);
  db_prepare(&q,
     "SELECT blob.rid, isRemoved, isExec, isLink, origname, newname, delta"
     "  FROM stashfile, blob WHERE stashid=%d AND blob.uuid=stashfile.hash"
     " UNION ALL SELECT 0, isRemoved, isExec, isLink, origname, newname, delta"
     "  FROM stashfile WHERE stashid=%d AND stashfile.hash IS NULL",
     stashid, stashid
  );
  while( db_step(&q)==SQLITE_ROW ){
    int rid = db_column_int(&q, 0);
    int isRemoved = db_column_int(&q, 1);
    int isLink = db_column_int(&q, 3);
    const char *zOrig = db_column_text(&q, 4);
    const char *zNew = db_column_text(&q, 5);
    char *zOPath = mprintf("%s%s", g.zLocalRoot, zOrig);
    Blob a, b;
    if( rid==0 ){
      db_ephemeral_blob(&q, 6, &a);
      if( !bWebpage ) fossil_print("ADDED %s\n", zNew);
      diff_print_index(zNew, pCfg, 0);
      diff_file_mem(&empty, &a, zNew, pCfg);
    }else if( isRemoved ){
      if( !bWebpage) fossil_print("DELETE %s\n", zOrig);
      diff_print_index(zNew, pCfg, 0);
      if( fBaseline ){
        content_get(rid, &a);
        diff_file_mem(&a, &empty, zOrig, pCfg);
      }
    }else{
      Blob delta;
      int isOrigLink = file_islink(zOPath);
      db_ephemeral_blob(&q, 6, &delta);
      if( !bWebpage ) fossil_print("CHANGED %s\n", zNew);
      if( !isOrigLink != !isLink ){
        diff_print_index(zNew, pCfg, 0);
        diff_print_filenames(zOrig, zNew, pCfg, 0);
        printf(DIFF_CANNOT_COMPUTE_SYMLINK);
      }else{
        content_get(rid, &a);
        blob_delta_apply(&a, &delta, &b);
        if( fBaseline ){
          diff_file_mem(&a, &b, zNew, pCfg);
        }else{
          pCfg->diffFlags ^= DIFF_INVERT;
          diff_file(&b, zOPath, zNew, pCfg, 0);
          pCfg->diffFlags ^= DIFF_INVERT;
        }
        blob_reset(&a);
        blob_reset(&b);
      }
      blob_reset(&delta);
    }
  }
  db_finalize(&q);
  diff_end(pCfg, 0);
}

/*
** Drop the indicated stash
*/
static void stash_drop(int stashid){
  db_multi_exec(
    "DELETE FROM stash WHERE stashid=%d;"
    "DELETE FROM stashfile WHERE stashid=%d;",
    stashid, stashid
  );
}

/*
** If zStashId is non-NULL then interpret is as a stash number and
** return that number.  Or throw a fatal error if it is not a valid
** stash number.  If it is NULL, return the most recent stash or
** throw an error if the stash is empty.
*/
static int stash_get_id(const char *zStashId){
  int stashid;
  if( zStashId==0 ){
    stashid = db_int(0, "SELECT max(stashid) FROM stash");
    if( stashid==0 ) fossil_fatal("empty stash");
  }else{
    stashid = atoi(zStashId);
    if( !db_exists("SELECT 1 FROM stash WHERE stashid=%d", stashid) ){
      fossil_fatal("no such stash: %s", zStashId);
    }
  }
  return stashid;
}

/*
** COMMAND: stash
**
** Usage: %fossil stash SUBCOMMAND ARGS...
**
** > fossil stash
** > fossil stash save ?-m|--comment COMMENT? ?FILES...?
** > fossil stash snapshot ?-m|--comment COMMENT? ?FILES...?
**
**      Save the current changes in the working tree as a new stash.
**      Then revert the changes back to the last check-in.  If FILES
**      are listed, then only stash and revert the named files.  The
**      "save" verb can be omitted if and only if there are no other
**      arguments.  The "snapshot" verb works the same as "save" but
**      omits the revert, keeping the checkout unchanged.
**
** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM?
**
**      List all changes sets currently stashed.  Show information about
**      individual files in each changeset if -v or --verbose is used.
**
** > fossil stash show|cat ?STASHID? ?DIFF-OPTIONS?
** > fossil stash gshow|gcat ?STASHID? ?DIFF-OPTIONS?
**
**      Show the contents of a stash as a diff against its baseline.
**      With gshow and gcat, gdiff-command is used instead of internal
**      diff logic.
**
** > fossil stash pop
** > fossil stash apply ?STASHID?
**
**      Apply STASHID or the most recently created stash to the current
**      working checkout.  The "pop" command deletes that changeset from
**      the stash after applying it but the "apply" command retains the
**      changeset.
**
** > fossil stash goto ?STASHID?
**
**      Update to the baseline checkout for STASHID then apply the
**      changes of STASHID.  Keep STASHID so that it can be reused
**      This command is undoable.
**
** > fossil stash drop|rm ?STASHID? ?-a|--all?
**
**      Forget everything about STASHID.  Forget the whole stash if the
**      -a|--all flag is used.  Individual drops are undoable but -a|--all
**      is not.
**
** > fossil stash diff ?STASHID? ?DIFF-OPTIONS?
** > fossil stash gdiff ?STASHID? ?DIFF-OPTIONS?
**
**      Show diffs of the current working directory and what that
**      directory would be if STASHID were applied. With gdiff,
**      gdiff-command is used instead of internal diff logic.
*/
void stash_cmd(void){
  const char *zCmd;
  int nCmd;
  int stashid = 0;
  undo_capture_command_line();
  db_must_be_within_tree();
  db_open_config(0, 0);
  db_begin_transaction();
  stash_tables_exist_and_current();
  if( g.argc<=2 ){
    zCmd = "save";
  }else{
    zCmd = g.argv[2];
  }
  nCmd = strlen(zCmd);
  if( memcmp(zCmd, "save", nCmd)==0 ){
    if( unsaved_changes(0)==0 ){
      fossil_fatal("nothing to stash");
    }
    stashid = stash_create();
    undo_disable();
    if( g.argc>=2 ){
      int nFile = db_int(0, "SELECT count(*) FROM stashfile WHERE stashid=%d",
                         stashid);
      char **newArgv = fossil_malloc( sizeof(char*)*(nFile+2) );
      int i = 2;
      Stmt q;
      db_prepare(&q,"SELECT origname FROM stashfile WHERE stashid=%d", stashid);
      while( db_step(&q)==SQLITE_ROW ){
        newArgv[i++] = mprintf("%s%s", g.zLocalRoot, db_column_text(&q, 0));
      }
      db_finalize(&q);
      newArgv[0] = g.argv[0];
      newArgv[1] = 0;
      g.argv = newArgv;
      g.argc = nFile+2;
      if( nFile==0 ) return;
    }
    /* Make sure the stash has committed before running the revert, so that
    ** we have a copy of the changes before deleting them. */
    db_commit_transaction();
    g.argv[1] = "revert";
    revert_cmd();
    fossil_print("stash %d saved\n", stashid);
    return;
  }else
  if( memcmp(zCmd, "snapshot", nCmd)==0 ){
    stash_create();
  }else
  if( memcmp(zCmd, "list", nCmd)==0 || memcmp(zCmd, "ls", nCmd)==0 ){
    Stmt q, q2;
    int n = 0, width;
    int verboseFlag = find_option("verbose","v",0)!=0;
    const char *zWidth = find_option("width","W",1);

    if( zWidth ){
      width = atoi(zWidth);
      if( (width!=0) && (width<=46) ){
        fossil_fatal("-W|--width value must be >46 or 0");
      }
    }else{
      width = -1;
    }
    if( !verboseFlag ){
      verboseFlag = find_option("detail","l",0)!=0; /* deprecated */
    }
    verify_all_options();
    db_prepare(&q,
       "SELECT stashid, hash, comment, datetime(ctime) FROM stash"
       " ORDER BY ctime"
    );
    if( verboseFlag ){
      db_prepare(&q2, "SELECT isAdded, isRemoved, origname, newname"
                      "  FROM stashfile WHERE stashid=$id");
    }
    while( db_step(&q)==SQLITE_ROW ){
      int stashid = db_column_int(&q, 0);
      const char *zCom;
      n++;
      fossil_print("%5d: [%.14s] on %s\n",
        stashid,
        db_column_text(&q, 1),
        db_column_text(&q, 3)
      );
      zCom = db_column_text(&q, 2);
      if( zCom && zCom[0] ){
        fossil_print("       ");
        comment_print(zCom, 0, 7, width, get_comment_format());
      }
      if( verboseFlag ){
        db_bind_int(&q2, "$id", stashid);
        while( db_step(&q2)==SQLITE_ROW ){
          int isAdded = db_column_int(&q2, 0);
          int isRemoved = db_column_int(&q2, 1);
          const char *zOrig = db_column_text(&q2, 2);
          const char *zNew = db_column_text(&q2, 3);
          if( isAdded ){
             fossil_print("          ADD %s\n", zNew);
          }else if( isRemoved ){
             fossil_print("          REMOVE %s\n", zOrig);
          }else if( fossil_strcmp(zOrig,zNew)!=0 ){
             fossil_print("          RENAME %s -> %s\n", zOrig, zNew);
          }else{
             fossil_print("          EDIT %s\n", zOrig);
          }
        }
        db_reset(&q2);
      }
    }
    db_finalize(&q);
    if( verboseFlag ) db_finalize(&q2);
    if( n==0 ) fossil_print("empty stash\n");
  }else
  if( memcmp(zCmd, "drop", nCmd)==0 || memcmp(zCmd, "rm", nCmd)==0 ){
    int allFlag = find_option("all", "a", 0)!=0;
    if( allFlag ){
      Blob ans;
      char cReply;
      prompt_user("This action is not undoable.  Continue (y/N)? ", &ans);
      cReply = blob_str(&ans)[0];
      if( cReply=='y' || cReply=='Y' ){
        db_multi_exec("DELETE FROM stash; DELETE FROM stashfile;");
      }
    }else if( g.argc>=4 ){
      int i;
      undo_begin();
      for(i=3; i<g.argc; i++){
        stashid = stash_get_id(g.argv[i]);
        undo_save_stash(stashid);
        stash_drop(stashid);
      }
      undo_finish();
    }else{
      undo_begin();
      undo_save_stash(0);
      stash_drop(stashid);
      undo_finish();
    }
  }else
  if( memcmp(zCmd, "pop", nCmd)==0 ){
    if( g.argc>3 ) usage("pop");
    stashid = stash_get_id(0);
    undo_begin();
    stash_apply(stashid, 0);
    undo_save_stash(stashid);
    undo_finish();
    stash_drop(stashid);
  }else
  if( memcmp(zCmd, "apply", nCmd)==0 ){
    if( g.argc>4 ) usage("apply STASHID");
    stashid = stash_get_id(g.argc==4 ? g.argv[3] : 0);
    undo_begin();
    stash_apply(stashid, 0);
    undo_finish();
  }else
  if( memcmp(zCmd, "goto", nCmd)==0 ){
    int nConflict;
    int vid;
    if( g.argc>4 ) usage("apply STASHID");
    stashid = stash_get_id(g.argc==4 ? g.argv[3] : 0);
    undo_begin();
    vid = db_int(0, "SELECT blob.rid FROM stash,blob"
                    " WHERE stashid=%d AND blob.uuid=stash.hash", stashid);
    nConflict = update_to(vid);
    stash_apply(stashid, nConflict);
    db_multi_exec("UPDATE vfile SET mtime=0 WHERE pathname IN "
                  "(SELECT origname FROM stashfile WHERE stashid=%d)",
                  stashid);
    undo_finish();
  }else
  if( memcmp(zCmd, "diff", nCmd)==0
   || memcmp(zCmd, "gdiff", nCmd)==0
   || memcmp(zCmd, "show", nCmd)==0
   || memcmp(zCmd, "gshow", nCmd)==0
   || memcmp(zCmd, "cat", nCmd)==0
   || memcmp(zCmd, "gcat", nCmd)==0
  ){
    int fBaseline = 0;
    DiffConfig DCfg;

    if( strstr(zCmd,"show")!=0 || strstr(zCmd,"cat")!=0 ){
      fBaseline = 1;
    }
    if( find_option("tk",0,0)!=0 ){
      db_close(0);
      diff_tk(fBaseline ? "stash show" : "stash diff", 3);
      return;
    }
    diff_options(&DCfg, zCmd[0]=='g', 0);
    stashid = stash_get_id(g.argc==4 ? g.argv[3] : 0);
    stash_diff(stashid, fBaseline, &DCfg);
  }else
  if( memcmp(zCmd, "help", nCmd)==0 ){
    g.argv[1] = "help";
    g.argv[2] = "stash";
    g.argc = 3;
    help_cmd();
  }else
  {
    usage("SUBCOMMAND ARGS...");
  }
  db_end_transaction(0);
}