From 7b7f7ab6ee17e23d2b7bfa8a41b128713788fcda Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Sat, 11 Mar 2023 12:43:34 +0000 Subject: [PATCH] restore: add `restore.defaultDestination` to configure what gets updated `git restore` takes `--worktree` and/or `--staged` options to specify which one of the working tree files and/or the index entries are updated. With neither option, the command, by default, updates the working tree files. If a user attempts to reset the index entries from HEAD, they may, by mistake, run `git restore` without the `--staged` option. When such a mistake happens, the work made in the working tree files that are not yet added to the index will be forever lost. This patch is intended to mitigate this. This is a trade-off between lost worktree changes, which may not be present anywhere else, and lost index modifications, which can be recreated. Introduce the `restore.defaultDestination` configuration variable, which can be set to one of "both", "index", or "worktree", useful for users who want to set it to "index" to avoid touching the working tree files by mistake. They now force themselves to use the "--worktree" option explicitly when they want to restore the working tree files. Signed-off-by: Hugo Sales --- Documentation/config.txt | 2 + Documentation/config/restore.txt | 16 ++++ Documentation/git-restore.txt | 17 ++-- builtin/checkout.c | 27 ++++++ t/t2070-restore.sh | 144 +++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 Documentation/config/restore.txt diff --git a/Documentation/config.txt b/Documentation/config.txt index 0e93aef86264db..4359c63794e6af 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -501,6 +501,8 @@ include::config/repack.txt[] include::config/rerere.txt[] +include::config/restore.txt[] + include::config/revert.txt[] include::config/safe.txt[] diff --git a/Documentation/config/restore.txt b/Documentation/config/restore.txt new file mode 100644 index 00000000000000..6a06310b4aa584 --- /dev/null +++ b/Documentation/config/restore.txt @@ -0,0 +1,16 @@ +restore.defaultDestination:: + Valid values: "worktree", "staged" or "both". Controls the default + behavior of `git restore` without `--worktree` or `--staged`. If + "worktree", `git restore` without `--worktree` or `--staged` is + equivalent to `git restore --worktree`. If "staged", `git restore` + without `--worktree` or `--staged` is equivalent to `git restore + --staged`. If "both", `git restore` without `--worktree` or `--staged` + is equivalent to `git restore --worktree --staged`. Adding an option + overrides the default, such that if the configuration variable is set to + "staged", specifying `--worktree` will only affect the worktree, not + both. This variable can be set to "staged" to help prevent accidentally + losing modifications to the worktree, caused by running `git restore .` + when `git restore --staged .` was intended. In this case, modifications + to the index would be lost, which could also be a significant amount of + work, so care is highly recommended. + See linkgit:git-restore[1] diff --git a/Documentation/git-restore.txt b/Documentation/git-restore.txt index 5964810caa4153..72acf5ee4ad6a9 100644 --- a/Documentation/git-restore.txt +++ b/Documentation/git-restore.txt @@ -14,14 +14,18 @@ SYNOPSIS DESCRIPTION ----------- -Restore specified paths in the working tree with some contents from a +Restore specified paths in the working tree and/or index with some contents from a restore source. If a path is tracked but does not exist in the restore source, it will be removed to match the source. -The command can also be used to restore the content in the index with +The command can be used to restore the content in the index with `--staged`, or restore both the working tree and the index with `--staged --worktree`. +The config options `restore.defaultDestination`, which accepts values "worktree", +"staged" or "both", can be used to control the default behavior for which +flag(s) apply if neither `--staged` nor `--worktree` is supplied. + By default, if `--staged` is given, the contents are restored from `HEAD`, otherwise from the index. Use `--source` to restore from a different commit. @@ -59,9 +63,12 @@ all modified paths. --worktree:: -S:: --staged:: - Specify the restore location. If neither option is specified, - by default the working tree is restored. Specifying `--staged` - will only restore the index. Specifying both restores both. + Specify the restore location. If neither option is specified, the + default depends on the `'restore.defaultDestination` config option, which + can be "worktree" (the default), "staged" or "both", to control which of + the two flags is assumed if none are given. Specifying `--worktree` will + only restore the worktree. Specifying `--staged` will only restore the + index. Specifying both restores both. -q:: --quiet:: diff --git a/builtin/checkout.c b/builtin/checkout.c index a5155cf55c1e51..9ab7fe9671468f 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1922,6 +1922,31 @@ int cmd_switch(int argc, const char **argv, const char *prefix) return ret; } +static const char *checkout_default_index_worktree; +static int git_restore_config(const char *var, const char *value, void *cb) +{ + struct checkout_opts *opts = cb; + + if (!strcmp(var, "restore.defaultdestination")) { + fprintf(stderr, value); + git_config_string(&checkout_default_index_worktree, var, value); + + if (!strcmp(checkout_default_index_worktree, "both")) { + opts->checkout_index = -2; /* default on */ + opts->checkout_worktree = -2; /* default on */ + } else if (!strcmp(checkout_default_index_worktree, "staged")) { + opts->checkout_index = -2; /* default on */ + opts->checkout_worktree = -1; /* default off */ + } else { + opts->checkout_index = -1; /* default off */ + opts->checkout_worktree = -2; /* default on */ + } + return 0; + } + return git_xmerge_config(var, value, NULL); +} + + int cmd_restore(int argc, const char **argv, const char *prefix) { struct checkout_opts opts; @@ -1950,6 +1975,8 @@ int cmd_restore(int argc, const char **argv, const char *prefix) opts.checkout_worktree = -2; /* default on */ opts.ignore_unmerged_opt = "--ignore-unmerged"; + git_config(git_restore_config, &opts); + options = parse_options_dup(restore_options); options = add_common_options(&opts, options); options = add_checkout_path_options(&opts, options); diff --git a/t/t2070-restore.sh b/t/t2070-restore.sh index 7c43ddf1d99714..7ad49bf357292e 100755 --- a/t/t2070-restore.sh +++ b/t/t2070-restore.sh @@ -137,4 +137,148 @@ test_expect_success 'restore --staged invalidates cache tree for deletions' ' test_must_fail git rev-parse HEAD:new1 ' +test_expect_success 'restore with restore.defaultDestination unset works as if --worktree given' ' + test_when_finished git reset --hard HEAD^ && + test_commit root-unset-restore.defaultDestination && + test_commit unset-restore.defaultDestination one one && + >one && + + git restore one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status && + + >one && + git add one && + git restore one && + git status --porcelain --untracked-files=no | grep "^M " && + + >one && + git add one && + git restore --worktree one && + git status --porcelain --untracked-files=no | grep "^M " && + + git restore --staged one && + git status --porcelain --untracked-files=no | grep "^ M" && + + >one && + git add one && + git restore --worktree --staged one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status +' + +test_expect_success 'restore with restore.defaultDestination set to worktree works as if --worktree given' ' + test_when_finished git reset --hard HEAD^ && + test_when_finished git config --unset restore.defaultDestination && + test_commit root-worktree-restore.defaultDestination && + test_commit worktree-restore.defaultDestination one one && + git config restore.defaultDestination worktree && + >one && + + git restore one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status && + + >one && + git add one && + git restore one && + git status --porcelain --untracked-files=no | grep "^M " && + + >one && + git add one && + git restore --worktree one && + git status --porcelain --untracked-files=no | grep "^M " && + + git restore --staged one && + git status --porcelain --untracked-files=no | grep "^ M" && + + >one && + git add one && + git restore --worktree --staged one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status +' + +test_expect_success 'restore with restore.defaultDestination set to staged works as if --staged given' ' + test_when_finished git reset --hard HEAD^ && + test_when_finished git config --unset restore.defaultDestination && + test_commit root-staged-restore.defaultDestination && + test_commit staged-restore.defaultDestination one one && + git config restore.defaultDestination staged && + >one && + + git restore one && + git status --porcelain --untracked-files=no | grep "^ M" && + + git restore --staged one && + git status --porcelain --untracked-files=no | grep "^ M" && + + git add one && + git restore one && + git status --porcelain --untracked-files=no | grep "^ M" && + + git add one && + git restore --staged one && + git status --porcelain --untracked-files=no | grep "^ M" && + + git restore --worktree one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status && + + >one && + git add one && + git restore --worktree --staged one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status +' + +test_expect_success 'restore with restore.defaultDestination set to both works as if --worktree --staged given' ' + test_when_finished git reset --hard HEAD^ && + test_when_finished git config --unset restore.defaultDestination && + test_commit root-both-restore.defaultDestination && + test_commit both-restore.defaultDestination one one && + git config restore.defaultDestination both && + >one && + + git restore one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status && + + >one && + git add one && + git restore --staged one && + git status --porcelain --untracked-files=no | grep "^ M" && + + git add one && + git restore one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status && + + >one && + git add one && + git restore --staged one && + git status --porcelain --untracked-files=no | grep "^ M" && + + git restore --worktree one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status && + + >one && + git add one && + git restore --worktree --staged one && + git status --porcelain --untracked-files=no >status && + test_must_be_empty status && + rm status +' + + test_done