/* ** 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 /* ** 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; idiffFlags & (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; i3 ) 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); }