summaryrefslogtreecommitdiff
path: root/src/undo.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/undo.c
downloadrelic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/undo.c')
-rw-r--r--src/undo.c543
1 files changed, 543 insertions, 0 deletions
diff --git a/src/undo.c b/src/undo.c
new file mode 100644
index 0000000..b06ce7f
--- /dev/null
+++ b/src/undo.c
@@ -0,0 +1,543 @@
+/*
+** 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 implements the undo/redo functionality.
+*/
+#include "config.h"
+#include "undo.h"
+
+#if INTERFACE
+/*
+** Possible return values from the undo_maybe_save() routine.
+*/
+#define UNDO_NONE (0) /* Placeholder only used to initialize vars. */
+#define UNDO_SAVED_OK (1) /* The specified file was saved successfully. */
+#define UNDO_DISABLED (2) /* File not saved, subsystem is disabled. */
+#define UNDO_INACTIVE (3) /* File not saved, subsystem is not active. */
+#define UNDO_TOOBIG (4) /* File not saved, it exceeded a size limit. */
+#endif
+
+/*
+** Undo the change to the file zPathname. zPathname is the pathname
+** of the file relative to the root of the repository. If redoFlag is
+** true the redo a change. If there is nothing to undo (or redo) then
+** this routine is a noop.
+*/
+static void undo_one(const char *zPathname, int redoFlag){
+ Stmt q;
+ char *zFullname;
+ db_prepare(&q,
+ "SELECT content, existsflag, isExe, isLink FROM undo"
+ " WHERE pathname=%Q AND redoflag=%d",
+ zPathname, redoFlag
+ );
+ if( db_step(&q)==SQLITE_ROW ){
+ int old_exists;
+ int new_exists;
+ int old_exe;
+ int new_exe;
+ int new_link;
+ int old_link;
+ Blob current;
+ Blob new;
+ zFullname = mprintf("%s%s", g.zLocalRoot, zPathname);
+ old_link = db_column_int(&q, 3);
+ new_exists = file_size(zFullname, RepoFILE)>=0;
+ new_link = file_islink(0);
+ if( new_exists ){
+ blob_read_from_file(&current, zFullname, RepoFILE);
+ new_exe = file_isexe(0,0);
+ }else{
+ blob_zero(&current);
+ new_exe = 0;
+ }
+ blob_zero(&new);
+ old_exists = db_column_int(&q, 1);
+ old_exe = db_column_int(&q, 2);
+ if( old_exists ){
+ db_ephemeral_blob(&q, 0, &new);
+ }
+ if( file_unsafe_in_tree_path(zFullname) ){
+ /* do nothing with this unsafe file */
+ }else if( old_exists ){
+ if( new_exists ){
+ fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname);
+ }else{
+ fossil_print("NEW %s\n", zPathname);
+ }
+ if( new_exists && (new_link || old_link) ){
+ file_delete(zFullname);
+ }
+ if( old_link ){
+ symlink_create(blob_str(&new), zFullname);
+ }else{
+ blob_write_to_file(&new, zFullname);
+ }
+ file_setexe(zFullname, old_exe);
+ }else{
+ fossil_print("DELETE %s\n", zPathname);
+ file_delete(zFullname);
+ }
+ blob_reset(&new);
+ free(zFullname);
+ db_finalize(&q);
+ db_prepare(&q,
+ "UPDATE undo SET content=:c, existsflag=%d, isExe=%d, isLink=%d,"
+ " redoflag=NOT redoflag"
+ " WHERE pathname=%Q",
+ new_exists, new_exe, new_link, zPathname
+ );
+ if( new_exists ){
+ db_bind_blob(&q, ":c", &current);
+ }
+ db_step(&q);
+ blob_reset(&current);
+ }
+ db_finalize(&q);
+}
+
+/*
+** Undo or redo changes to the filesystem. Undo the changes in the
+** same order that they were originally carried out - undo the oldest
+** change first and undo the most recent change last.
+*/
+static void undo_all_filesystem(int redoFlag){
+ Stmt q;
+ db_prepare(&q,
+ "SELECT pathname FROM undo"
+ " WHERE redoflag=%d"
+ " ORDER BY rowid",
+ redoFlag
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zPathname = db_column_text(&q, 0);
+ undo_one(zPathname, redoFlag);
+ }
+ db_finalize(&q);
+}
+
+/*
+** Undo or redo all undoable or redoable changes.
+*/
+static void undo_all(int redoFlag){
+ int ucid;
+ int ncid;
+ undo_all_filesystem(redoFlag);
+ db_multi_exec(
+ "CREATE TEMP TABLE undo_vfile_2 AS SELECT * FROM vfile;"
+ "DELETE FROM vfile;"
+ "INSERT INTO vfile SELECT * FROM undo_vfile;"
+ "DELETE FROM undo_vfile;"
+ "INSERT INTO undo_vfile SELECT * FROM undo_vfile_2;"
+ "DROP TABLE undo_vfile_2;"
+ "CREATE TEMP TABLE undo_vmerge_2 AS SELECT * FROM vmerge;"
+ "DELETE FROM vmerge;"
+ "INSERT INTO vmerge SELECT * FROM undo_vmerge;"
+ "DELETE FROM undo_vmerge;"
+ "INSERT INTO undo_vmerge SELECT * FROM undo_vmerge_2;"
+ "DROP TABLE undo_vmerge_2;"
+ );
+ if( db_table_exists("localdb", "undo_stash") ){
+ if( redoFlag ){
+ db_multi_exec(
+ "DELETE FROM stash WHERE stashid IN (SELECT stashid FROM undo_stash);"
+ "DELETE FROM stashfile"
+ " WHERE stashid NOT IN (SELECT stashid FROM stash);"
+ );
+ }else{
+ db_multi_exec(
+ "INSERT OR IGNORE INTO stash SELECT * FROM undo_stash;"
+ "INSERT OR IGNORE INTO stashfile SELECT * FROM undo_stashfile;"
+ );
+ }
+ }
+ ncid = db_lget_int("undo_checkout", 0);
+ ucid = db_lget_int("checkout", 0);
+ db_lset_int("undo_checkout", ucid);
+ db_set_checkout(ncid, 1);
+}
+
+/*
+** Reset the undo memory.
+*/
+void undo_reset(void){
+ static const char zSql[] =
+ @ DROP TABLE IF EXISTS undo;
+ @ DROP TABLE IF EXISTS undo_vfile;
+ @ DROP TABLE IF EXISTS undo_vmerge;
+ @ DROP TABLE IF EXISTS undo_stash;
+ @ DROP TABLE IF EXISTS undo_stashfile;
+ ;
+ db_exec_sql(zSql);
+ db_lset_int("undo_available", 0);
+ db_lset_int("undo_checkout", 0);
+}
+
+/*
+** The following variable stores the original command-line of the
+** command that is a candidate to be undone.
+*/
+static char *undoCmd = 0;
+
+/*
+** This flag is true if we are in the process of collecting file changes
+** for undo. When this flag is false, undo_save() is a no-op.
+**
+** The undoDisable flag, if set, prevents undo from being activated.
+*/
+static int undoActive = 0;
+static int undoDisable = 0;
+
+
+/*
+** Capture the current command-line and store it as part of the undo
+** state. This routine is called before options are extracted from the
+** command-line so that we can record the complete command-line.
+*/
+void undo_capture_command_line(void){
+ Blob cmdline;
+ int i;
+ if( undoCmd!=0 || undoDisable ) return;
+ blob_zero(&cmdline);
+ for(i=1; i<g.argc; i++){
+ if( i>1 ) blob_append(&cmdline, " ", 1);
+ blob_append(&cmdline, g.argv[i], -1);
+ }
+ undoCmd = blob_str(&cmdline);
+}
+
+/*
+** Begin capturing a snapshot that can be undone.
+*/
+void undo_begin(void){
+ int cid;
+ static const char zSql[] =
+ @ CREATE TABLE localdb.undo(
+ @ pathname TEXT UNIQUE, -- Name of the file
+ @ redoflag BOOLEAN, -- 0 for undoable. 1 for redoable
+ @ existsflag BOOLEAN, -- True if the file exists
+ @ isExe BOOLEAN, -- True if the file is executable
+ @ isLink BOOLEAN, -- True if the file is symlink
+ @ content BLOB -- Saved content
+ @ );
+ @ CREATE TABLE localdb.undo_vfile AS SELECT * FROM vfile;
+ @ CREATE TABLE localdb.undo_vmerge AS SELECT * FROM vmerge;
+ ;
+ if( undoDisable ) return;
+ undo_reset();
+ db_exec_sql(zSql);
+ cid = db_lget_int("checkout", 0);
+ db_lset_int("undo_checkout", cid);
+ db_lset_int("undo_available", 1);
+ db_lset("undo_cmdline", undoCmd);
+ undoActive = 1;
+}
+
+/*
+** Permanently disable undo
+*/
+void undo_disable(void){
+ undoDisable = 1;
+}
+
+/*
+** This flag is true if one or more files have changed and have been
+** recorded in the undo log but the undo log has not yet been committed.
+**
+** If a fatal error occurs and this flag is set, that means we should
+** rollback all the filesystem changes.
+*/
+static int undoNeedRollback = 0;
+
+/*
+** Save the current content of the file zPathname so that it
+** will be undoable. The name is relative to the root of the
+** tree.
+*/
+void undo_save(const char *zPathname){
+ if( undoDisable ) return;
+ if( undo_maybe_save(zPathname, -1)!=UNDO_SAVED_OK ){
+ fossil_fatal("failed to save undo information for path: %s",
+ zPathname);
+ }
+}
+
+/*
+** Possibly save the current content of the file zPathname so
+** that it will be undoable. The name is relative to the root
+** of the tree. The limit argument may be used to specify the
+** maximum size for the file to be saved. If the size of the
+** specified file exceeds this size limit (in bytes), it will
+** not be saved and an appropriate code will be returned.
+**
+** WARNING: Please do NOT call this function with a limit
+** value less than zero, call the undo_save()
+** function instead.
+**
+** The return value of this function will always be one of the
+** following codes:
+**
+** UNDO_SAVED_OK: The specified file was saved successfully.
+**
+** UNDO_DISABLED: The specified file was NOT saved, because the
+** "undo subsystem" is disabled. This error may
+** indicate that a call to undo_disable() was
+** issued.
+**
+** UNDO_INACTIVE: The specified file was NOT saved, because the
+** "undo subsystem" is not active. This error
+** may indicate that a call to undo_begin() is
+** missing.
+**
+** UNDO_TOOBIG: The specified file was NOT saved, because it
+** exceeded the specified size limit. It is
+** impossible for this value to be returned if
+** the specified size limit is less than zero
+** (i.e. unlimited).
+*/
+int undo_maybe_save(const char *zPathname, i64 limit){
+ char *zFullname;
+ i64 size;
+ int result;
+
+ if( undoDisable ) return UNDO_DISABLED;
+ if( !undoActive ) return UNDO_INACTIVE;
+ zFullname = mprintf("%s%s", g.zLocalRoot, zPathname);
+ size = file_size(zFullname, RepoFILE);
+ if( limit<0 || size<=limit ){
+ int existsFlag = (size>=0);
+ int isLink = file_islink(zFullname);
+ Stmt q;
+ Blob content;
+ db_prepare(&q,
+ "INSERT OR IGNORE INTO"
+ " undo(pathname,redoflag,existsflag,isExe,isLink,content)"
+ " VALUES(%Q,0,%d,%d,%d,:c)",
+ zPathname, existsFlag, file_isexe(zFullname,RepoFILE), isLink
+ );
+ if( existsFlag ){
+ blob_read_from_file(&content, zFullname, RepoFILE);
+ db_bind_blob(&q, ":c", &content);
+ }
+ db_step(&q);
+ db_finalize(&q);
+ if( existsFlag ){
+ blob_reset(&content);
+ }
+ undoNeedRollback = 1;
+ result = UNDO_SAVED_OK;
+ }else{
+ result = UNDO_TOOBIG;
+ }
+ free(zFullname);
+ return result;
+}
+
+/*
+** Returns an error message for the undo_maybe_save() return code.
+** Currently, this function assumes that the caller is using the
+** returned error message in a context prefixed with "because".
+*/
+const char *undo_save_message(int rc){
+ static char zRc[32];
+
+ switch( rc ){
+ case UNDO_NONE: return "undo is disabled for this operation";
+ case UNDO_SAVED_OK: return "the save operation was successful";
+ case UNDO_DISABLED: return "the undo subsystem is disabled";
+ case UNDO_INACTIVE: return "the undo subsystem is inactive";
+ case UNDO_TOOBIG: return "the file is too big";
+ default: {
+ sqlite3_snprintf(sizeof(zRc), zRc, "of error code %d", rc);
+ }
+ }
+ return zRc;
+}
+
+/*
+** Make the current state of stashid undoable.
+*/
+void undo_save_stash(int stashid){
+ db_multi_exec(
+ "CREATE TABLE IF NOT EXISTS localdb.undo_stash"
+ " AS SELECT * FROM stash WHERE 0;"
+ "INSERT INTO undo_stash"
+ " SELECT * FROM stash WHERE stashid=%d;",
+ stashid
+ );
+ db_multi_exec(
+ "CREATE TABLE IF NOT EXISTS localdb.undo_stashfile"
+ " AS SELECT * FROM stashfile WHERE 0;"
+ "INSERT INTO undo_stashfile"
+ " SELECT * FROM stashfile WHERE stashid=%d;",
+ stashid
+ );
+}
+
+/*
+** Complete the undo process is one is currently in process.
+*/
+void undo_finish(void){
+ if( undoActive ){
+ if( undoNeedRollback ){
+ fossil_print(" \"fossil undo\" is available to undo changes"
+ " to the working checkout.\n");
+ }
+ undoActive = 0;
+ undoNeedRollback = 0;
+ }
+}
+
+/*
+** This routine is called when the process aborts due to an error.
+** If an undo was being accumulated but was not finished, attempt
+** to rollback all of the filesystem changes.
+**
+** This rollback occurs, for example, if an "update" or "merge" operation
+** could not run to completion because a file that needed to be written
+** was locked or had permissions turned off.
+*/
+void undo_rollback(void){
+ if( !undoNeedRollback ) return;
+ assert( undoActive );
+ undoNeedRollback = 0;
+ undoActive = 0;
+ fossil_print("Rolling back prior filesystem changes...\n");
+ undo_all_filesystem(0);
+}
+
+/*
+** COMMAND: undo
+** COMMAND: redo*
+**
+** Usage: %fossil undo ?OPTIONS? ?FILENAME...?
+** or: %fossil redo ?OPTIONS? ?FILENAME...?
+**
+** The undo command reverts the changes caused by the previous command
+** if the previous command is one of the following:
+** * fossil update
+** * fossil merge
+** * fossil revert
+** * fossil stash pop
+** * fossil stash apply
+** * fossil stash drop
+** * fossil stash goto
+** * fossil clean (*see note below*)
+**
+** Note: The "fossil clean" command only saves state for files less than
+** 10MiB in size and so if fossil clean deleted files larger than that,
+** then "fossil undo" will not recover the larger files.
+**
+** If FILENAME is specified then restore the content of the named
+** file(s) but otherwise leave the update or merge or revert in effect.
+** The redo command undoes the effect of the most recent undo.
+**
+** If the -n|--dry-run option is present, no changes are made and instead
+** the undo or redo command explains what actions the undo or redo would
+** have done had the -n|--dry-run been omitted.
+**
+** If the most recent command is not one of those listed as undoable,
+** then the undo command might try to restore the state to be what it was
+** prior to the last undoable command, or it might be a no-op. If in
+** doubt about what the undo command will do, first run it with the -n
+** option.
+**
+** A single level of undo/redo is supported. The undo/redo stack
+** is cleared by the commit and check-out commands. Other commands may
+** or may not clear the undo stack.
+**
+** Future versions of Fossil might add new commands to the set of commands
+** that are undoable.
+**
+** Options:
+** -n|--dry-run Do not make changes, but show what would be done
+**
+** See also: [[commit]], [[status]]
+*/
+void undo_cmd(void){
+ int isRedo = g.argv[1][0]=='r';
+ int undo_available;
+ int dryRunFlag = find_option("dry-run", "n", 0)!=0;
+ const char *zCmd = isRedo ? "redo" : "undo";
+
+ if( !dryRunFlag ){
+ dryRunFlag = find_option("explain", 0, 0)!=0;
+ }
+ db_must_be_within_tree();
+ verify_all_options();
+ db_begin_transaction();
+ undo_available = db_lget_int("undo_available", 0);
+ if( dryRunFlag ){
+ if( undo_available==0 ){
+ fossil_print("No undo or redo is available\n");
+ }else{
+ Stmt q;
+ int nChng = 0;
+ const char *zArticle = undo_available==1 ? "An" : "A";
+ zCmd = undo_available==1 ? "undo" : "redo";
+ fossil_print("%s %s is available for the following command:\n\n"
+ " %s %s\n\n",
+ zArticle, zCmd, g.argv[0], db_lget("undo_cmdline", "???"));
+ db_prepare(&q,
+ "SELECT existsflag, pathname FROM undo ORDER BY pathname"
+ );
+ while( db_step(&q)==SQLITE_ROW ){
+ if( nChng==0 ){
+ fossil_print("The following file changes would occur if the "
+ "command above is %sne:\n\n", zCmd);
+ }
+ nChng++;
+ fossil_print("%s %s\n",
+ db_column_int(&q,0) ? "UPDATE" : "DELETE",
+ db_column_text(&q, 1)
+ );
+ }
+ db_finalize(&q);
+ if( nChng==0 ){
+ fossil_print("No file changes would occur with this undo/redo.\n");
+ }
+ }
+ }else{
+ int vid1 = db_lget_int("checkout", 0);
+ int vid2;
+ if( g.argc==2 ){
+ if( undo_available!=(1+isRedo) ){
+ fossil_fatal("nothing to %s", zCmd);
+ }
+ undo_all(isRedo);
+ db_lset_int("undo_available", 2-isRedo);
+ }else if( g.argc>=3 ){
+ int i;
+ if( undo_available==0 ){
+ fossil_fatal("nothing to %s", zCmd);
+ }
+ for(i=2; i<g.argc; i++){
+ const char *zFile = g.argv[i];
+ Blob path;
+ file_tree_name(zFile, &path, 0, 1);
+ undo_one(blob_str(&path), isRedo);
+ blob_reset(&path);
+ }
+ }
+ vid2 = db_lget_int("checkout", 0);
+ if( vid1!=vid2 ){
+ fossil_print("--------------------\n");
+ show_common_info(vid2, "updated-to:", 1, 0);
+ }
+ }
+ db_end_transaction(0);
+}