diff mbox series

Add `restore.defaultLocation` option

Message ID pull.1467.git.git.1678578153640.gitgitgadget@gmail.com (mailing list archive)
State New, archived
Headers show
Series Add `restore.defaultLocation` option | expand

Commit Message

Hugo Sales March 11, 2023, 11:42 p.m. UTC
From: Hugo Sales <hugo@hsal.es>

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 <hugo@hsal.es>
---
    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 avarab@gmail.com, Jeff King peff@peff.net,
    Victoria Dye vdye@github.com
    
    ------------------------------------------------------------------------
    
    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


base-commit: 725f57037d81e24eacfda6e59a19c60c0b4c8062

Comments

Junio C Hamano March 12, 2023, 9:32 p.m. UTC | #1
"Hugo Sales via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Hugo Sales <hugo@hsal.es>
>
> This options allows control over which of `--worktree` or `--staged` is
> applied when `git restore` is invoked with neither

We do not want to do this.  Tutorials and documents will be written
assuming the official default, and those users who set it to use a
different default, only because they are told to use it before they
know better, would waste a lot of time wondering why the procedures
they read from tutorials and documents would not work for them.
Even an expert who is asked to help a novice, who has this variable
set, would end up getting confused becaue "git restore" without the
explict options does not work as they expect.

Individual users are welcome to use "[alias] ri = restore --staged"
and this will hurt nobody.
Junio C Hamano March 13, 2023, 6:02 p.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> "Hugo Sales via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> From: Hugo Sales <hugo@hsal.es>
>>
>> This options allows control over which of `--worktree` or `--staged` is
>> applied when `git restore` is invoked with neither
>
> We do not want to do this.  Tutorials and documents will be written
> assuming the official default, ...

Well, I think this change is no different from any other
configuration option and it may be OK.

My initial reaction primarily came from the fact that the choice
between the index and the working tree is so fundamental aspect of
the behaviour of the command that people who wrote their script
would be relying on it not to change.  But given that the command is
still marked as experimental, as long as the new behaviour is
clearly documented to warn those who care *not* to rely on the
default behaviour and tell them to instead always give explicitly
these "--worktree" and/or "--staged" options, it would be OK.

This actually is a more important tangent, but if you think the
command invites mistakes from users who forget to give "--staged",
it may indicate that the command is too overloaded, and the UI might
be improved by removing the feature from this command and instead
encouraging people to use "git reset" to futz with the contents of
the index.  I dunno.
Junio C Hamano March 13, 2023, 11:11 p.m. UTC | #3
"Hugo Sales via GitGitGadget" <gitgitgadget@gmail.com> writes:

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

As to the form, perhaps

	Subject: [PATCH] restore: make it configurable what gets updated

cf. Documentation/SubmittingPatches[[describe-changes]]

Also on substance,

 * "option" usually refers to the --option given on the command
   line; when you mean "configuration variable", it is a misleading
   word to use.

 * "restore" command deals with two quite conceptually different
   locations.  Where it gets the contents from (i.e. the source
   location), and where the contents are used to update (i.e. the
   destination location).  The ".defaultLocation" is a poor name for
   a configuration variable because it does not convey which one is
   meant.

> From: Hugo Sales <hugo@hsal.es>
>
> 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.

We usually describe the status quo (what the current system does),
what bad things can happen with the current system, and then what
change is proposed to improve the situation, in this order.  So the
above is backwards.  Perhaps like

    "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 options, the command by
    default updates the working tree files.

    A user who wanted to reset the index entries from HEAD 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.

    Introduce the restore.defaultDestination configuration variable,
    which can be set to one of "both", "index", or "worktree", to
    help those 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.

or something along that line would make it more like our log messages.

Note that even though I wrote the above by guessing what your
motivation behind this change is, I do not very much agree with the
third paragraph myself.  I think a better solution would be to teach
these users to use "git reset -- <path>" when they want to reset the
index entries, and not run the "git restore" command.


> +	This
> +	option can be used to prevent accidentally losing work by running `git
> +	restore .` when `git restore --staged .` was intended.

This is better left out, as it cuts both ways.  With it set to
'index', this option will clobber the index entries the user
carefully prepared so far with "git add -p" and friends, when the
user wanted to update the working tree files (either from the index
or from an existing commit) by running "git restore", which will
lose work.

If we wanted to advertise it as a feature, then the sentence should
say something like

	This variable can be set to 'index' to prevent accidentally
	losing work ...

"can be used" is too vague when you are talking about setting it to
a particular value.  IOW, setting it to any other value would *NOT*
prevent "git restore" from behaving differently from "git resetore --staged".

Again, I do not think it is a good idea to sell it as a feature in
the first place, as it cuts both ways.

> 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

Shouldn't it be "and/or"?

> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index a5155cf55c1..5067753030b 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -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;

Why?


> 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 &&

Lose SP between ">" and "one", its redirection target.

cf. Documentation/CodingGuidelines, look for "Redirection operators
should be written with space before, but no space after them." and
study the entire paragraph.

> +	git restore one &&
> +	test -z $(git status --porcelain --untracked-files=no) &&

This (together with many other uses of "git status" in the new
tests) will not catch a segfaulting "git status".

Thanks.
Hugo Sales March 14, 2023, 11:51 p.m. UTC | #4
I think I addressed your comments in the patch below.

> I think a better solution would be to teach
> these users to use "git reset -- <path>" when they want to reset the
> index entries, and not run the "git restore" command.

I read _somewhere_ that `git restore` was the Modern Replacement for `git
checkout` and `git reset`, along with `git switch`. I do find it more intuitive,
too. Friends I talked with liked this change, as they've also read similar
things and lost work due to a missing `--staged`, so they stopped using it. I
find `git reset` somewhat confusing, as I use it for `git reset --soft HEAD~`
and `git reset --hard <commit-ish>`, so I don't really associate it with the
same usage as `git restore`.

> +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")) {
> +        git_config_string(&checkout_default_index_worktree, var, value);

I'm not sure if this use of `git_config_string` is required, as `value` seems to
contain the desired value, but this seems to be how it's done elsewhere.

> > +    git restore one &&
> > +    test -z $(git status --porcelain --untracked-files=no) &&
>
> This (together with many other uses of "git status" in the new
> tests) will not catch a segfaulting "git status".

I replaced this with `test_must_be_empty`, I'm not sure if that's what you
meant. I wasn't able to find a version that doesn't need to write to a file.

I'm aware I'm not supposed to send patches as an attachment, but I spent at least 3 hours
today and 3-4 more the day I sent the first patch, and it just doesn't work, so I hope this works



From ec878a84db4dc7aabbc5cddf0897b717ac772494 Mon Sep 17 00:00:00 2001
From: Hugo Sales <hugo@hsal.es>
Date: Sat, 11 Mar 2023 12:43:34 +0000
Subject: [PATCH v2] 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 <hugo@hsal.es>
---
I think I addressed your comments in the patch below.

> I think a better solution would be to teach
> these users to use "git reset -- <path>" when they want to reset the
> index entries, and not run the "git restore" command.

I read _somewhere_ that `git restore` was the Modern Replacement for `git
checkout` and `git reset`, along with `git switch`. I do find it more intuite,
too. Friends I talked with liked this change, as they've also read similar
things and lost work due to a missing `--staged`, so they stopped using it. I
find `git reset` somewhat confusing, as I use it for `git reset --soft HEAD~`
and `git reset --hard <commit-ish>`, so I don't really associate it with the
same usage as `git restore`.

> +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")) {
> +        git_config_string(&checkout_default_index_worktree, var, value);

I'm not sure if this use of `git_config_string` is required, as `value` seems to
contain the desired value, but this seems to be how it's done elsewhere.

> > +    git restore one &&
> > +    test -z $(git status --porcelain --untracked-files=no) &&
>
> This (together with many other uses of "git status" in the new
> tests) will not catch a segfaulting "git status".

I replaced this with `test_must_be_empty`, I'm not sure if that's what you
meant. I wasn't able to find a version that doesn't need to write to a file.

P.S. I hope the following reaches you correctly, as I'm still struggling to
connect to mailbox.org with send-email.

 Documentation/config.txt         |   2 +
 Documentation/config/restore.txt |  16 ++++
 Documentation/git-restore.txt    |  17 ++--
 builtin/checkout.c               |  25 ++++++
 t/t2070-restore.sh               | 143 +++++++++++++++++++++++++++++++
 5 files changed, 198 insertions(+), 5 deletions(-)
 create mode 100644 Documentation/config/restore.txt

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0e93aef862..4359c63794 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 0000000000..6a06310b4a
--- /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 5964810caa..391c367d32 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 configuration vairable `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 destination. If neither option is specified, the default
+    depends on the `'restore.defaultDestination` configuration variable,
+    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 a5155cf55c..4a9131ec29 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1922,6 +1922,29 @@ 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")) {
+        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 +1973,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 7c43ddf1d9..c3a35dd878 100755
--- a/t/t2070-restore.sh
+++ b/t/t2070-restore.sh
@@ -137,4 +137,147 @@ 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
Hugo Sales March 26, 2023, 10:53 a.m. UTC | #5
Wanted to add that since `git status` suggests using `git restore --staged` to
reset index entries, I'm sure it has happened many times that a user ran `git
restore` and lost work
diff mbox series

Patch

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