Subject: [PATCH] Add `restore.defaultLocation` option

From: "Hugo Sales via GitGitGadget" <[email-masked]>
Date: Sat, 11 Mar 2023 23:42:33 +0000

From: Hugo Sales <[email-masked]>

This options allows control over which of `--worktree` or `--staged` is
applied when `git restore` is invoked with neither

This patch is intended to reduce lost work to accidental `git restore .`
when `git restore --staged .` was intended.

Signed-off-by: Hugo Sales <[email-masked]>
---
    Add restore.defaultLocation option
    
    This options allows control over which of --worktree or --staged is
    applied when git restore is invoked with neither
    
    This patch is intended to reduce lost work to accidental git restore .
    when git restore --staged . was intended.
    
    CC: Ævar Arnfjörð Bjarmason [email-masked], Jeff King [email-masked],
    Victoria Dye [email-masked]
    
    ------------------------------------------------------------------------
    
    I tried to send with git send-email, but I'm having problems. My mail
    provider is mailbox.org and I'm getting Command unknown: 'AUTH' at
    /usr/lib/git-core/git-send-email line 1691. Apologies if it actually
    went through.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1467%2Fsomeonewithpc%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1467/someonewithpc/master-v1
Pull-Request: https://github.com/git/git/pull/1467

 Documentation/config.txt         |   2 +
 Documentation/config/restore.txt |  13 ++++
 Documentation/git-restore.txt    |  17 +++--
 builtin/checkout.c               |  27 +++++++
 t/t2070-restore.sh               | 124 +++++++++++++++++++++++++++++++
 5 files changed, 178 insertions(+), 5 deletions(-)
 create mode 100644 Documentation/config/restore.txt

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0e93aef8626..4359c63794e 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 00000000000..479fd13ca24
--- /dev/null
+++ b/Documentation/config/restore.txt
@@ -0,0 +1,13 @@
+restore.defaultLocation::
+	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 option is set to "staged",
+	specifying `--worktree` will only affect the worktree, not both. This
+	option can be used to prevent accidentally losing work by running `git
+	restore .` when `git restore --staged .` was intended.
+	See linkgit:git-restore[1]
diff --git a/Documentation/git-restore.txt b/Documentation/git-restore.txt
index 5964810caa4..28165861f55 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 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.defaultLocation`, 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.defaultLocation` 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 a5155cf55c1..5067753030b 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1922,6 +1922,30 @@ 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.defaultlocation")) {
+		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;
@@ -1942,6 +1966,7 @@ int cmd_restore(int argc, const char **argv, const char *prefix)
 	struct branch_info new_branch_info = { 0 };
 
 	memset(&opts, 0, sizeof(opts));
+
 	opts.accept_ref = 0;
 	opts.accept_pathspec = 1;
 	opts.empty_pathspec_ok = 0;
@@ -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 7c43ddf1d99..6e9b06e0bf4 100755
--- a/t/t2070-restore.sh
+++ b/t/t2070-restore.sh
@@ -137,4 +137,128 @@ test_expect_success 'restore --staged invalidates cache tree for deletions' '
 	test_must_fail git rev-parse HEAD:new1
 '
 
+test_expect_success 'restore with restore.defaultLocation unset works as if --worktree given' '
+	test_when_finished git reset --hard HEAD^ &&
+	test_commit root-unset-restore.defaultLocation &&
+	test_commit unset-restore.defaultLocation one one &&
+	> one &&
+
+	git restore one &&
+	test -z $(git status --porcelain --untracked-files=no) &&
+
+	> 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 &&
+	test -z $(git status --porcelain --untracked-files=no)
+'
+
+test_expect_success 'restore with restore.defaultLocation set to worktree works as if --worktree given' '
+	test_when_finished git reset --hard HEAD^ &&
+	test_when_finished git config --unset restore.defaultLocation &&
+	test_commit root-worktree-restore.defaultLocation &&
+	test_commit worktree-restore.defaultLocation one one &&
+	git config restore.defaultLocation worktree &&
+	> one &&
+
+	git restore one &&
+	test -z $(git status --porcelain --untracked-files=no) &&
+
+	> 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 &&
+	test -z $(git status --porcelain --untracked-files=no)
+'
+
+test_expect_success 'restore with restore.defaultLocation set to staged works as if --staged given' '
+	test_when_finished git reset --hard HEAD^ &&
+	test_when_finished git config --unset restore.defaultLocation &&
+	test_commit root-staged-restore.defaultLocation &&
+	test_commit staged-restore.defaultLocation one one &&
+	git config restore.defaultLocation 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 &&
+	test -z $(git status --porcelain --untracked-files=no) &&
+
+	> one &&
+	git add one &&
+	git restore --worktree --staged one &&
+	test -z $(git status --porcelain --untracked-files=no)
+'
+
+test_expect_success 'restore with restore.defaultLocation set to both works as if --worktree --staged given' '
+	test_when_finished git reset --hard HEAD^ &&
+	test_when_finished git config --unset restore.defaultLocation &&
+	test_commit root-both-restore.defaultLocation &&
+	test_commit both-restore.defaultLocation one one &&
+	git config restore.defaultLocation both &&
+	> one &&
+
+	git restore one &&
+	test -z $(git status --porcelain --untracked-files=no) &&
+
+	> one &&
+	git add one &&
+	git restore --staged one &&
+	git status --porcelain --untracked-files=no | grep "^ M"  &&
+
+	git add one &&
+	git restore one &&
+	test -z $(git status --porcelain --untracked-files=no) &&
+
+	> one &&
+	git add one &&
+	git restore --staged one &&
+	git status --porcelain --untracked-files=no | grep "^ M" &&
+
+	git restore --worktree one &&
+	test -z $(git status --porcelain --untracked-files=no) &&
+
+	> one &&
+	git add one &&
+	git restore --worktree --staged one &&
+	test -z $(git status --porcelain --untracked-files=no)
+'
+
+
 test_done

base-commit: 725f57037d81e24eacfda6e59a19c60c0b4c8062
-- 
gitgitgadget