Subject: [PATCH v2 00/16] Add new command 'restore'

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Thu, 11 Apr 2019 20:12:02 +0700

This is the companion of "git switch" and is based on that topic.
This command peforms the "checkout paths" from git-checkout, git-reset
and also has a third mode to reset only worktree, leaving the index
alone.

For new people not aware of previous discussions, this command is
supposed to be a friendlier replacement for "git checkout" and
hopefully fixes some warts that have grown over the time in
git-checkout. For this reason, the last patch starts to recommend "git
restore" everywhere.

v2 should address all the comments from v1. I still haven't done that
--intent-to-add thing yet even though I'd like to make it default
behavior too. Anyway changes are

- --index is renamed to --staged to avoid conflict with --index from
  git-apply, which has different meaning.

- lots of document fixes. The comparison between restore/revert/reset
  is added in git.txt instead. I want to avoid duplicate the same
  thing on three man pages.

- --force is dropped. --ignore-unmerged takes its place.

- --recurse-submodules is dropped. I've concluded that restoring files
  across submodule boundary is not exactly supported yet. Let's leave it
  for later.

- git-rm learns about --staged as an alias of --cached (in fact it's
  more the other way around). This is to keep suggestions consistent
  because we tell people to do "git foo --staged" everywhere.

- git-reset and git-diff are moved to other groups in "git help"
  output.

Nguyễn Thái Ngọc Duy (16):
  checkout: split part of it to new command 'restore'
  restore: take tree-ish from --source option instead
  restore: make pathspec mandatory
  restore: disable overlay mode by default
  checkout: factor out worktree checkout code
  restore: add --worktree and --staged
  restore: reject invalid combinations with --staged
  restore: default to --source=HEAD when only --staged is specified
  restore: replace --force with --ignore-unmerged
  restore: support --patch
  t: add tests for restore
  completion: support restore
  user-manual.txt: prefer 'merge --abort' over 'reset --hard'
  doc: promote "git restore"
  rm: add --staged as alias for --cached
  help: move git-diff and git-reset to different groups

 .gitignore                             |   1 +
 Documentation/config/interactive.txt   |   3 +-
 Documentation/git-checkout.txt         |   3 +-
 Documentation/git-clean.txt            |   2 +-
 Documentation/git-commit.txt           |   2 +-
 Documentation/git-format-patch.txt     |   2 +-
 Documentation/git-reset.txt            |  13 +-
 Documentation/git-restore.txt (new)    | 183 +++++++++++++++
 Documentation/git-revert.txt           |   7 +-
 Documentation/git-rm.txt               |   7 +-
 Documentation/git.txt                  |  20 ++
 Documentation/gitcli.txt               |   4 +-
 Documentation/giteveryday.txt          |   5 +-
 Documentation/gittutorial-2.txt        |   4 +-
 Documentation/gittutorial.txt          |   2 +-
 Documentation/user-manual.txt          |  14 +-
 Makefile                               |   1 +
 builtin.h                              |   1 +
 builtin/checkout.c                     | 299 +++++++++++++++++++------
 builtin/clone.c                        |   2 +-
 builtin/commit.c                       |   2 +-
 builtin/rm.c                           |   7 +-
 command-list.txt                       |   7 +-
 contrib/completion/git-completion.bash |  15 ++
 git-add--interactive.perl              |  52 +++++
 git.c                                  |   1 +
 t/lib-patch-mode.sh                    |  12 +
 t/t2070-restore.sh (new +x)            |  99 ++++++++
 t/t2071-restore-patch.sh (new +x)      | 110 +++++++++
 t/t3600-rm.sh                          |   6 +-
 t/t7508-status.sh                      |  84 +++----
 t/t7512-status-help.sh                 |  20 +-
 wt-status.c                            |  30 ++-
 33 files changed, 848 insertions(+), 172 deletions(-)
 create mode 100644 Documentation/git-restore.txt
 create mode 100755 t/t2070-restore.sh
 create mode 100755 t/t2071-restore-patch.sh

Diff:

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index ee0d164d0e..a294652dd6 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -570,7 +570,7 @@ $ git add frotz
 
 SEE ALSO
 --------
-linkgit:git-switch[1]
+linkgit:git-switch[1],
 linkgit:git-restore[1]
 
 GIT
diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt
index e7ac8eb9e8..7628193284 100644
--- a/Documentation/git-commit.txt
+++ b/Documentation/git-commit.txt
@@ -56,7 +56,7 @@ summary of what is included by any of the above for the next
 commit by giving the same set of parameters (options and paths).
 
 If you make a commit and then find a mistake immediately after
-that, you can recover from it with 'git restore' or 'git reset'.
+that, you can recover from it with 'git reset'.

 OPTIONS
@@ -359,7 +359,7 @@ When recording your own work, the contents of modified files in
 your working tree are temporarily stored to a staging area
 called the "index" with 'git add'.  A file can be
 reverted back, only in the index but not in the working tree,
-to that of the last commit with `git restore --index <file>`,
+to that of the last commit with `git restore --staged <file>`,
 which effectively reverts 'git add' and prevents the changes to
 this file from participating in the next commit.  After building
 the state to be committed incrementally with these commands,
diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt
index 71c9fe3af3..01bfcecf69 100644
--- a/Documentation/git-format-patch.txt
+++ b/Documentation/git-format-patch.txt
@@ -422,7 +422,7 @@ One way to test if your MUA is set up correctly is:
 
     $ git fetch <project> master:test-apply
     $ git switch test-apply
-    $ git restore --source=HEAD --index --worktree :/
+    $ git restore --source=HEAD --staged --worktree :/
     $ git am a.patch
 
 If it does not apply correctly, there can be various reasons.
diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt
index b70281677f..633d71d36a 100644
--- a/Documentation/git-reset.txt
+++ b/Documentation/git-reset.txt
@@ -25,7 +25,8 @@ The `<tree-ish>`/`<commit>` defaults to `HEAD` in all forms.
 	the current branch.)
 +
 This means that `git reset <paths>` is the opposite of `git add
-<paths>`.
+<paths>`. This command is equivalent to
+`git restore [--source=<tree-ish>] --staged <paths>...`.
 +
 After running `git reset <paths>` to update the index entry, you can
 use linkgit:git-restore[1] to check the contents out of the index to
@@ -86,8 +87,8 @@ but carries forward unmerged index entries.
 	changes, reset is aborted.
 --
 
-If you want to undo a commit other than the latest on a branch,
-linkgit:git-revert[1] is your friend.
+See "Reset, restore and revert" in linkgit:git[1] for the differences
+between the three commands.

 OPTIONS
diff --git a/Documentation/git-restore.txt b/Documentation/git-restore.txt
index a667a5ced4..b608f3f360 100644
--- a/Documentation/git-restore.txt
+++ b/Documentation/git-restore.txt
@@ -8,48 +8,60 @@ git-restore - Restore working tree files
 SYNOPSIS
 --------
 [verse]
-'git restore' [<options>] [--source=<revision>] <pathspec>...
-'git restore' (-p|--patch) [--source=<revision>] [<pathspec>...]
+'git restore' [<options>] [--source=<tree>] [--staged] [--worktree] <pathspec>...
+'git restore' (-p|--patch) [<options>] [--source=<tree>] [--staged] [--worktree] [<pathspec>...]
 
 DESCRIPTION
 -----------
-Restore paths in the working tree by replacing with the contents in
-the restore source or remove if the paths do not exist in the restore
-source. The source is by default the index but could be from a commit.
-The command can also optionally restore content in the index from a
-commit.
+Restore specified paths in the working tree 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.
 
-When a `<revision>` is given, the paths that match the `<pathspec>` are
-updated both in the index and in the working tree.
+The command can also be used to restore the content in the index with
+`--staged`, or restore both the working tree and the index with
+`--staged --worktree`.
+
+By default, the restore sources for working tree and the index are the
+index and `HEAD` respectively. `--source` could be used to specify a
+commit as the restore source.
+
+See "Reset, restore and revert" in linkgit:git[1] for the differences
+between the three commands.
 
 OPTIONS
 -------
--s<tree>::
+-s <tree>::
 --source=<tree>::
 	Restore the working tree files with the content from the given
-	tree or any revision that leads to a tree (e.g. a commit or a
-	branch).
+	tree. It is common to specify the source tree by naming a
+	commit, branch or tag associated with it.
++
+If not specified, the default restore source for the working tree is
+the index, and the default restore source for the index index is
+`HEAD`. When both `--staged` and `--worktree` are specified,
+`--source` must also be specified.
 
 -p::
 --patch::
 	Interactively select hunks in the difference between the
-	`<revision>` (or the index, if unspecified) and the working
-	tree. See the ``Interactive Mode'' section of linkgit:git-add[1]
-	to learn how to operate the `--patch` mode.
+	restore source and the restore location. See the ``Interactive
+	Mode'' section of linkgit:git-add[1] to learn how to operate
+	the `--patch` mode.
++
+Note that `--patch` can accept no pathspec and will prompt to restore
+all modified paths.
 
 -W::
 --worktree::
--I::
---index::
+-S::
+--staged::
 	Specify the restore location. If neither option is specified,
-	by default the working tree is restored. If `--index` is
-	specified without `--worktree` or `--source`, `--source=HEAD`
-	is implied. These options only make sense to use with
-	`--source`.
+	by default the working tree is restored. Specifying `--staged`
+	will only restore the index. Specifying both restores both.
 
 -q::
 --quiet::
-	Quiet, suppress feedback messages.
+	Quiet, suppress feedback messages. Implies `--no-progress`.
 
 --progress::
 --no-progress::
@@ -58,16 +70,10 @@ OPTIONS
 	is specified. This flag enables progress reporting even if not
 	attached to a terminal, regardless of `--quiet`.
 
--f::
---force::
-	If `--source` is not specified, unmerged entries are left alone
-	and will not fail the operation. Unmerged entries are always
-	replaced if `--source` is specified, regardless of `--force`.
-
 --ours::
 --theirs::
-	Check out stage #2 ('ours') or #3 ('theirs') for unmerged
-	paths.
+	When restoring files in the working tree from the index, use
+	stage #2 ('ours') or #3 ('theirs') for unmerged paths.
 +
 Note that during `git rebase` and `git pull --rebase`, 'ours' and
 'theirs' may appear swapped. See the explanation of the same options
@@ -75,67 +81,64 @@ in linkgit:git-checkout[1] for details.
 
 -m::
 --merge::
-	Recreate the conflicted merge in the specified paths.
+	When restoring files on the working tree from the index,
+	recreate the conflicted merge in the unmerged paths.
 
 --conflict=<style>::
 	The same as `--merge` option above, but changes the way the
-	conflicting hunks are presented, overriding the merge.conflictStyle
-	configuration variable.  Possible values are "merge" (default)
-	and "diff3" (in addition to what is shown by "merge" style,
-	shows the original contents).
+	conflicting hunks are presented, overriding the
+	`merge.conflictStyle` configuration variable.  Possible values
+	are "merge" (default) and "diff3" (in addition to what is
+	shown by "merge" style, shows the original contents).
+
+--ignore-unmerged::
+	When restoring files on the working tree from the index, do
+	not abort the operation if there are unmerged entries and
+	neither `--ours`, `--theirs`, `--merge` or `--conflict` is
+	specified. Unmerged paths on the working tree are left alone.
 
 --ignore-skip-worktree-bits::
-	In sparse checkout mode, by default update only entries
+	In sparse checkout mode, by default is to only update entries
 	matched by `<pathspec>` and sparse patterns in
 	$GIT_DIR/info/sparse-checkout. This option ignores the sparse
-	patterns and unconditionally restores any files in `<pathspec>`.
-
---recurse-submodules::
---no-recurse-submodules::
-	Using `--recurse-submodules` will update the content of all initialized
-	submodules according to the commit recorded in the superproject. If
-	local modifications in a submodule would be overwritten the checkout
-	will fail unless `-f` is used. If nothing (or `--no-recurse-submodules`)
-	is used, the work trees of submodules will not be updated.
-	Just like linkgit:git-submodule[1], this will detach the
-	submodules HEAD.
+	patterns and unconditionally restores any files in
+	`<pathspec>`.
 
 --overlay::
 --no-overlay::
-	In overlay mode, `git checkout` never removes files from the
-	index or the working tree. In no-overlay mode, files that
-	appear in the index and working tree, but not in `--source` tree
-	are removed, to make them match `<tree-ish>` exactly. The
-	default is no-overlay mode.
+	In overlay mode, the command never removes files when
+	restoring. In no-overlay mode, tracked files that do not
+	appear in the `--source` tree are removed, to make them match
+	`<tree>` exactly. The default is no-overlay mode.
 
 EXAMPLES
 --------
 
-The following sequence checks out the `master` branch, reverts
-the `Makefile` to two revisions back, deletes hello.c by
-mistake, and gets it back from the index.
+The following sequence switches to the `master` branch, reverts the
+`Makefile` to two revisions back, deletes hello.c by mistake, and gets
+it back from the index.
 
 ------------
 $ git switch master
 $ git restore --source master~2 Makefile  <1>
 $ rm -f hello.c
-$ git restore hello.c                   <2>
+$ git restore hello.c                     <2>
 ------------
 
 <1> take a file out of another commit
 <2> restore hello.c from the index
 
-If you want to check out _all_ C source files out of the index,
-you can say
+If you want to restore _all_ C source files to match the version in
+the index, you can say
 
 ------------
 $ git restore '*.c'
 ------------
 
 Note the quotes around `*.c`.  The file `hello.c` will also be
-checked out, even though it is no longer in the working tree,
-because the file globbing is used to match entries in the index
-(not in the working tree by the shell).
+restored, even though it is no longer in the working tree, because the
+file globbing is used to match entries in the index (not in the
+working tree by the shell).
 
 To restore all files in the current directory
 
@@ -144,28 +147,36 @@ $ git restore .
 ------------
 
 or to restore all working tree files with 'top' pathspec magic (see
-linkgit::gitglossary[7])
+linkgit:gitglossary[7])
 
 ------------
 $ git restore :/
 ------------
 
-To restore a file in the index only (this is the same as using
-"git reset")
+To restore a file in the index to match the version in `HEAD` (this is
+the same as using linkgit:git-reset[1])
+
+------------
+$ git restore --staged hello.c
+------------
+
+or you can restore both the index and the working tree (this the same
+as using linkgit:git-checkout[1])
 
 ------------
-$ git restore --index hello.c
+$ git restore --source=HEAD --staged --worktree hello.c
 ------------
 
-or you can restore both the index and the working tree
+or the short form which is more practical but less readable:
 
 ------------
-$ git restore --source=HEAD --index --worktree hello.c
+$ git restore -s@ -SW hello.c
 ------------
 
 SEE ALSO
 --------
-linkgit:git-checkout[1]
+linkgit:git-checkout[1],
+linkgit:git-reset[1]
 
 GIT
 ---
diff --git a/Documentation/git-revert.txt b/Documentation/git-revert.txt
index c38bc54439..9aadc36881 100644
--- a/Documentation/git-revert.txt
+++ b/Documentation/git-revert.txt
@@ -27,9 +27,12 @@ throw away all uncommitted changes in your working directory, you
 should see linkgit:git-reset[1], particularly the `--hard` option.  If
 you want to extract specific files as they were in another commit, you
 should see linkgit:git-restore[1], specifically the `--source`
-option  Take care with these alternatives as
+option. Take care with these alternatives as
 both will discard uncommitted changes in your working directory.
 
+See "Reset, restore and revert" in linkgit:git[1] for the differences
+between the three commands.
+
 OPTIONS
 -------
 <commit>...::
diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt
index b5c46223c4..4271fc5eaa 100644
--- a/Documentation/git-rm.txt
+++ b/Documentation/git-rm.txt
@@ -8,7 +8,7 @@ git-rm - Remove files from the working tree and from the index
 SYNOPSIS
 --------
 [verse]
-'git rm' [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch] [--quiet] [--] <file>...
+'git rm' [-f | --force] [-n] [-r] [--staged] [--ignore-unmatch] [--quiet] [--] <file>...
 
 DESCRIPTION
 -----------
@@ -55,10 +55,11 @@ OPTIONS
 	the list of files, (useful when filenames might be mistaken
 	for command-line options).
 
+--staged::
 --cached::
 	Use this option to unstage and remove paths only from the index.
-	Working tree files, whether modified or not, will be
-	left alone.
+	Working tree files, whether modified or not, will be left
+	alone. `--cached` is synonym for `--staged`.
 
 --ignore-unmatch::
 	Exit with a zero status even if no files matched.
diff --git a/Documentation/git.txt b/Documentation/git.txt
index 00156d64aa..fbed007354 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -210,6 +210,26 @@ people via patch over e-mail.
 
 include::cmds-foreignscminterface.txt[]
 
+Reset, restore and revert
+~~~~~~~~~~~~~~~~~~~~~~~~~
+There are three commands with similar names: `git reset`,
+`git restore` and `git revert`.
+
+* linkgit:git-revert[1] is about making a new commit that reverts the
+  changes made by other commits.
+
+* linkgit:git-restore[1] is about restoring files in the working tree
+  from either the index or another commit. This command does not
+  update your branch. The command can also be used to restore files in
+  the index from another commit.
+
+* linkgit:git-reset[1] is about updating your branch, moving the tip
+  in order to add or remove commits from the branch. This operation
+  changes the commit history.
++
+`git reset` can also be used to restore the index, overlapping with
+`git restore`.
+
 
 Low-level commands (plumbing)
 -----------------------------
diff --git a/Documentation/giteveryday.txt b/Documentation/giteveryday.txt
index 79517b22f9..1bd919f92b 100644
--- a/Documentation/giteveryday.txt
+++ b/Documentation/giteveryday.txt
@@ -51,8 +51,7 @@ following commands.
 
   * linkgit:git-commit[1] to advance the current branch.
 
-  * linkgit:git-reset[1] and linkgit:git-restore[1] (with
-    pathname parameters) to undo changes.
+  * linkgit:git-restore[1] to undo changes.
 
   * linkgit:git-merge[1] to merge between local branches.
 
diff --git a/Documentation/gittutorial-2.txt b/Documentation/gittutorial-2.txt
index e412841488..8bdb7d0bd3 100644
--- a/Documentation/gittutorial-2.txt
+++ b/Documentation/gittutorial-2.txt
@@ -370,7 +370,7 @@ situation:
 $ git status
 On branch master
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   closing.txt
 
diff --git a/Documentation/gittutorial.txt b/Documentation/gittutorial.txt
index 6266ca21b4..59ef5cef1f 100644
--- a/Documentation/gittutorial.txt
+++ b/Documentation/gittutorial.txt
@@ -110,7 +110,7 @@ $ git status
 On branch master
 Changes to be committed:
 Your branch is up to date with 'origin/master'.
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   file1
 	modified:   file2
diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt
index 02713886f0..8bce75b2cf 100644
--- a/Documentation/user-manual.txt
+++ b/Documentation/user-manual.txt
@@ -1446,7 +1446,7 @@ mistake, you can return the entire working tree to the last committed
 state with
 
 -------------------------------------------------
-$ git restore --index --worktree :/
+$ git restore --staged --worktree :/
 -------------------------------------------------
 
 If you make a commit that you later wish you hadn't, there are two
diff --git a/builtin/checkout.c b/builtin/checkout.c
index e8896ddbca..bed79ae595 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -38,7 +38,7 @@ static const char * const switch_branch_usage[] = {
 	NULL,
 };
 
-static const char * const restore_files_usage[] = {
+static const char * const restore_usage[] = {
 	N_("git restore [<options>] [--source=<branch>] <file>..."),
 	NULL,
 };
@@ -68,6 +68,8 @@ struct checkout_opts {
 	int empty_pathspec_ok;
 	int checkout_index;
 	int checkout_worktree;
+	const char *ignore_unmerged_opt;
+	int ignore_unmerged;
 
 	const char *new_branch;
 	const char *new_branch_force;
@@ -409,8 +411,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 	if (opts->new_branch_log)
 		die(_("'%s' cannot be used with updating paths"), "-l");
 
-	if (opts->force && opts->patch_mode)
-		die(_("'%s' cannot be used with updating paths"), "-f");
+	if (opts->ignore_unmerged && opts->patch_mode)
+		die(_("'%s' cannot be used with updating paths"),
+		    opts->ignore_unmerged_opt);
 
 	if (opts->force_detach)
 		die(_("'%s' cannot be used with updating paths"), "--detach");
@@ -418,8 +421,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 	if (opts->merge && opts->patch_mode)
 		die(_("'%s' cannot be used with %s"), "--merge", "--patch");
 
-	if (opts->force && opts->merge)
-		die(_("'%s' cannot be used with %s"), "-f", "-m");
+	if (opts->ignore_unmerged && opts->merge)
+		die(_("'%s' cannot be used with %s"),
+		    opts->ignore_unmerged_opt, "-m");
 
 	if (opts->new_branch)
 		die(_("Cannot update paths and switch to branch '%s' at the same time."),
@@ -427,12 +431,22 @@ static int checkout_paths(const struct checkout_opts *opts,
 
 	if (!opts->checkout_worktree && !opts->checkout_index)
 		die(_("neither '%s' or '%s' is specified"),
-		    "--index", "--worktree");
+		    "--staged", "--worktree");
 
-	if (!opts->source_tree && !opts->checkout_worktree)
+	if (!opts->checkout_worktree && !opts->from_treeish)
 		die(_("'%s' must be used when '%s' is not specified"),
 		    "--worktree", "--source");
 
+	if (opts->checkout_index && !opts->checkout_worktree &&
+	    opts->writeout_stage)
+		die(_("'%s' or '%s' cannot be used with %s"),
+		    "--ours", "--theirs", "--staged");
+
+	if (opts->checkout_index && !opts->checkout_worktree &&
+	    opts->merge)
+		die(_("'%s' or '%s' cannot be used with %s"),
+		    "--merge", "--conflict", "--staged");
+
 	if (opts->patch_mode) {
 		const char *patch_mode;
 
@@ -443,7 +457,8 @@ static int checkout_paths(const struct checkout_opts *opts,
 		else if (!opts->checkout_index && opts->checkout_worktree)
 			patch_mode = "--patch=worktree";
 		else
-			BUG("either flag must have been set");
+			BUG("either flag must have been set, worktree=%d, index=%d",
+			    opts->checkout_worktree, opts->checkout_index);
 		return run_add_interactive(revision, patch_mode, &opts->pathspec);
 	}
 
@@ -486,8 +501,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 		if (ce->ce_flags & CE_MATCHED) {
 			if (!ce_stage(ce))
 				continue;
-			if (opts->force) {
-				warning(_("path '%s' is unmerged"), ce->name);
+			if (opts->ignore_unmerged) {
+				if (!opts->quiet)
+					warning(_("path '%s' is unmerged"), ce->name);
 			} else if (opts->writeout_stage) {
 				errs |= check_stage(opts->writeout_stage, ce, pos, opts->overlay_mode);
 			} else if (opts->merge) {
@@ -1405,8 +1421,6 @@ static struct option *add_common_options(struct checkout_opts *opts,
 			    "checkout", "control recursive updating of submodules",
 			    PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater },
 		OPT_BOOL(0, "progress", &opts->show_progress, N_("force progress reporting")),
-		OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"),
-			   PARSE_OPT_NOCOMPLETE),
 		OPT_BOOL('m', "merge", &opts->merge, N_("perform a 3-way merge with the new branch")),
 		OPT_STRING(0, "conflict", &opts->conflict_style, N_("style"),
 			   N_("conflict style (merge or diff3)")),
@@ -1424,6 +1438,8 @@ static struct option *add_common_switch_branch_options(
 		OPT_BOOL('d', "detach", &opts->force_detach, N_("detach HEAD at named commit")),
 		OPT_SET_INT('t', "track",  &opts->track, N_("set upstream info for new branch"),
 			BRANCH_TRACK_EXPLICIT),
+		OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"),
+			   PARSE_OPT_NOCOMPLETE),
 		OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unparented branch")),
 		OPT_BOOL_F(0, "overwrite-ignore", &opts->overwrite_ignore,
 			   N_("update ignored files (default)"),
@@ -1493,8 +1509,11 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 		opts->merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", opts->conflict_style, NULL);
 	}
-	if (opts->force)
+	if (opts->force) {
 		opts->discard_changes = 1;
+		opts->ignore_unmerged_opt = "--force";
+		opts->ignore_unmerged = 1;
+	}
 
 	if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1)
 		die(_("-b, -B and --orphan are mutually exclusive"));
@@ -1516,8 +1535,8 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 	if (opts->checkout_index < 0 || opts->checkout_worktree < 0)
 		BUG("these flags should be non-negative by now");
 	/*
-	 * convenient shortcut: "git restore --index" equals
-	 * "git restore --index --source HEAD"
+	 * convenient shortcut: "git restore --staged" equals
+	 * "git restore --staged --source HEAD"
 	 */
 	if (!opts->from_treeish && opts->checkout_index && !opts->checkout_worktree)
 		opts->from_treeish = "HEAD";
@@ -1587,7 +1606,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 
 	if (opts->accept_pathspec && !opts->empty_pathspec_ok && !argc &&
 	    !opts->patch_mode)	/* patch mode is special */
-		die(_("pathspec is required"));
+		die(_("you must specify path(s) to restore"));
 
 	if (argc) {
 		parse_pathspec(&opts->pathspec, 0,
@@ -1737,10 +1756,12 @@ int cmd_restore(int argc, const char **argv, const char *prefix)
 	struct option restore_options[] = {
 		OPT_STRING('s', "source", &opts.from_treeish, "<tree-ish>",
 			   N_("where the checkout from")),
-		OPT_BOOL('I', "index", &opts.checkout_index,
+		OPT_BOOL('S', "staged", &opts.checkout_index,
 			   N_("restore the index")),
 		OPT_BOOL('W', "worktree", &opts.checkout_worktree,
 			   N_("restore the working tree (default)")),
+		OPT_BOOL(0, "ignore-unmerged", &opts.ignore_unmerged,
+			 N_("ignore unmerged entries")),
 		OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode")),
 		OPT_END()
 	};
@@ -1753,13 +1774,14 @@ int cmd_restore(int argc, const char **argv, const char *prefix)
 	opts.overlay_mode = 0;
 	opts.checkout_index = -1;    /* default off */
 	opts.checkout_worktree = -2; /* default on */
+	opts.ignore_unmerged_opt = "--ignore-unmerged";
 
 	options = parse_options_dup(restore_options);
 	options = add_common_options(&opts, options);
 	options = add_checkout_path_options(&opts, options);
 
 	ret = checkout_main(argc, argv, prefix, &opts,
-			    options, restore_files_usage);
+			    options, restore_usage);
 	FREE_AND_NULL(options);
 	return ret;
 }
diff --git a/builtin/commit.c b/builtin/commit.c
index c530264e63..fa5982cc86 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -1676,7 +1676,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
 	if (commit_index_files())
 		die(_("repository has been updated, but unable to write\n"
 		      "new_index file. Check that disk is not full and quota is\n"
-		      "not exceeded, and then \"git restore --index :/\" to recover."));
+		      "not exceeded, and then \"git restore --staged :/\" to recover."));
 
 	if (git_env_bool(GIT_TEST_COMMIT_GRAPH, 0))
 		write_commit_graph_reachable(get_object_directory(), 0, 0);
diff --git a/builtin/rm.c b/builtin/rm.c
index db85b33982..47c8eb100b 100644
--- a/builtin/rm.c
+++ b/builtin/rm.c
@@ -217,7 +217,7 @@ static int check_local_mod(struct object_id *head, int index_only)
 			     "staged in the index:",
 			     "the following files have changes "
 			     "staged in the index:", files_cached.nr),
-			  _("\n(use --cached to keep the file,"
+			  _("\n(use --staged to keep the file,"
 			    " or -f to force removal)"),
 			  &errs);
 	string_list_clear(&files_cached, 0);
@@ -226,7 +226,7 @@ static int check_local_mod(struct object_id *head, int index_only)
 			  Q_("the following file has local modifications:",
 			     "the following files have local modifications:",
 			     files_local.nr),
-			  _("\n(use --cached to keep the file,"
+			  _("\n(use --staged to keep the file,"
 			    " or -f to force removal)"),
 			  &errs);
 	string_list_clear(&files_local, 0);
@@ -240,7 +240,8 @@ static int ignore_unmatch = 0;
 static struct option builtin_rm_options[] = {
 	OPT__DRY_RUN(&show_only, N_("dry run")),
 	OPT__QUIET(&quiet, N_("do not list removed files")),
-	OPT_BOOL( 0 , "cached",         &index_only, N_("only remove from the index")),
+	OPT_BOOL( 0 , "staged",         &index_only, N_("only remove from the index")),
+	OPT_BOOL( 0 , "cached",         &index_only, N_("synonym for --staged")),
 	OPT__FORCE(&force, N_("override the up-to-date check"), PARSE_OPT_NOCOMPLETE),
 	OPT_BOOL('r', NULL,             &recursive,  N_("allow recursive removal")),
 	OPT_BOOL( 0 , "ignore-unmatch", &ignore_unmatch,
diff --git a/command-list.txt b/command-list.txt
index cf8dccb439..a9ac72bef4 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -81,7 +81,7 @@ git-cvsimport                           foreignscminterface
 git-cvsserver                           foreignscminterface
 git-daemon                              synchingrepositories
 git-describe                            mainporcelain
-git-diff                                mainporcelain           history
+git-diff                                mainporcelain           info
 git-diff-files                          plumbinginterrogators
 git-diff-index                          plumbinginterrogators
 git-diff-tree                           plumbinginterrogators
@@ -150,7 +150,7 @@ git-repack                              ancillarymanipulators           complete
 git-replace                             ancillarymanipulators           complete
 git-request-pull                        foreignscminterface             complete
 git-rerere                              ancillaryinterrogators
-git-reset                               mainporcelain           worktree
+git-reset                               mainporcelain           history
 git-restore                             mainporcelain           worktree
 git-revert                              mainporcelain
 git-rev-list                            plumbinginterrogators
diff --git a/t/t2070-restore.sh b/t/t2070-restore.sh
index df91bf54bc..73ea13ede9 100755
--- a/t/t2070-restore.sh
+++ b/t/t2070-restore.sh
@@ -23,11 +23,6 @@ test_expect_success 'restore without pathspec is not ok' '
 	test_must_fail git restore --source=first
 '
 
-test_expect_success 'restore -p without pathspec is fine' '
-	echo q >cmd &&
-	git restore -p <cmd
-'
-
 test_expect_success 'restore a file, ignoring branch of same name' '
 	cat one >expected &&
 	echo dirty >>one &&
@@ -35,7 +30,7 @@ test_expect_success 'restore a file, ignoring branch of same name' '
 	test_cmp expected one
 '
 
-test_expect_success 'restore a file on worktree from another branch' '
+test_expect_success 'restore a file on worktree from another ref' '
 	test_when_finished git reset --hard &&
 	git cat-file blob first:./first.t >expected &&
 	git restore --source=first first.t &&
@@ -45,33 +40,60 @@ test_expect_success 'restore a file on worktree from another branch' '
 	test_cmp expected actual
 '
 
-test_expect_success 'restore a file in the index from another branch' '
+test_expect_success 'restore a file in the index from another ref' '
 	test_when_finished git reset --hard &&
 	git cat-file blob first:./first.t >expected &&
-	git restore --source=first --index first.t &&
+	git restore --source=first --staged first.t &&
 	git show :first.t >actual &&
 	test_cmp expected actual &&
 	git cat-file blob HEAD:./first.t >expected &&
 	test_cmp expected first.t
 '
 
-test_expect_success 'restore a file in both the index and worktree from another branch' '
+test_expect_success 'restore a file in both the index and worktree from another ref' '
 	test_when_finished git reset --hard &&
 	git cat-file blob first:./first.t >expected &&
-	git restore --source=first --index --worktree first.t &&
+	git restore --source=first --staged --worktree first.t &&
 	git show :first.t >actual &&
 	test_cmp expected actual &&
 	test_cmp expected first.t
 '
 
-test_expect_success 'restore --index uses HEAD as source' '
+test_expect_success 'restore --staged uses HEAD as source' '
 	test_when_finished git reset --hard &&
 	git cat-file blob :./first.t >expected &&
 	echo index-dirty >>first.t &&
 	git add first.t &&
-	git restore --index first.t &&
+	git restore --staged first.t &&
 	git cat-file blob :./first.t >actual &&
 	test_cmp expected actual
 '
 
+test_expect_success 'restore --ignore-unmerged ignores unmerged entries' '
+	git init unmerged &&
+	(
+		cd unmerged &&
+		echo one >unmerged &&
+		echo one >common &&
+		git add unmerged common &&
+		git commit -m common &&
+		git switch -c first &&
+		echo first >unmerged &&
+		git commit -am first &&
+		git switch -c second master &&
+		echo second >unmerged &&
+		git commit -am second &&
+		test_must_fail git merge first &&
+
+		echo dirty >>common &&
+		test_must_fail git restore . &&
+
+		git restore --ignore-unmerged --quiet . >output 2>&1 &&
+		git diff common >diff-output &&
+		: >empty &&
+		test_cmp empty output &&
+		test_cmp empty diff-output
+	)
+'
+
 test_done
diff --git a/t/t2071-restore-patch.sh b/t/t2071-restore-patch.sh
index 46ebcb2413..98b2476e7c 100755
--- a/t/t2071-restore-patch.sh
+++ b/t/t2071-restore-patch.sh
@@ -16,6 +16,11 @@ test_expect_success PERL 'setup' '
 	save_head
 '
 
+test_expect_success PERL 'restore -p without pathspec is fine' '
+	echo q >cmd &&
+	git restore -p <cmd
+'
+
 # note: bar sorts before dir/foo, so the first 'n' is always to skip 'bar'
 
 test_expect_success PERL 'saying "n" does nothing' '
diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh
index 04e5d42bd3..5686032b8c 100755
--- a/t/t3600-rm.sh
+++ b/t/t3600-rm.sh
@@ -797,7 +797,7 @@ test_expect_success 'rm file with local modification' '
 	cat >expect <<-\EOF &&
 	error: the following file has local modifications:
 	    foo.txt
-	(use --cached to keep the file, or -f to force removal)
+	(use --staged to keep the file, or -f to force removal)
 	EOF
 	git commit -m "testing rm 3" &&
 	echo content3 >foo.txt &&
@@ -819,7 +819,7 @@ test_expect_success 'rm file with changes in the index' '
 	cat >expect <<-\EOF &&
 	error: the following file has changes staged in the index:
 	    foo.txt
-	(use --cached to keep the file, or -f to force removal)
+	(use --staged to keep the file, or -f to force removal)
 	EOF
 	git reset --hard &&
 	echo content5 >foo.txt &&
@@ -845,7 +845,7 @@ test_expect_success 'rm files with two different errors' '
 	(use -f to force removal)
 	error: the following file has changes staged in the index:
 	    bar1.txt
-	(use --cached to keep the file, or -f to force removal)
+	(use --staged to keep the file, or -f to force removal)
 	EOF
 	echo content >foo1.txt &&
 	git add foo1.txt &&
diff --git a/t/t7508-status.sh b/t/t7508-status.sh
index d5cf0185b7..738f3df2f9 100755
--- a/t/t7508-status.sh
+++ b/t/t7508-status.sh
@@ -71,7 +71,7 @@ test_expect_success 'setup' '
 '
 
 test_expect_success 'status (1)' '
-	test_i18ngrep "use \"git rm --cached <file>\.\.\.\" to unstage" output
+	test_i18ngrep "use \"git rm --staged <file>\.\.\.\" to unstage" output
 '
 
 strip_comments () {
@@ -94,7 +94,7 @@ test_expect_success 'status --column' '
 #   (use "git pull" to merge the remote branch into yours)
 #
 # Changes to be committed:
-#   (use "git restore --index <file>..." to unstage)
+#   (use "git restore --staged <file>..." to unstage)
 #
 #	new file:   dir2/added
 #
@@ -128,7 +128,7 @@ cat >expect <<\EOF
 #   (use "git pull" to merge the remote branch into yours)
 #
 # Changes to be committed:
-#   (use "git restore --index <file>..." to unstage)
+#   (use "git restore --staged <file>..." to unstage)
 #
 #	new file:   dir2/added
 #
@@ -278,7 +278,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 
@@ -347,7 +347,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 
@@ -420,7 +420,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 
@@ -484,7 +484,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 
@@ -542,7 +542,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 
@@ -605,7 +605,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   ../dir2/added
 
@@ -676,7 +676,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	<GREEN>new file:   dir2/added<RESET>
 
@@ -802,7 +802,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 
@@ -852,7 +852,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   dir1/modified
 
@@ -896,7 +896,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 	new file:   sm
@@ -956,7 +956,7 @@ and have 1 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	new file:   dir2/added
 	new file:   sm
@@ -1068,7 +1068,7 @@ and have 2 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --source=HEAD^1 --index <file>..." to unstage)
+  (use "git restore --source=HEAD^1 --staged <file>..." to unstage)
 
 	new file:   dir2/added
 	new file:   sm
@@ -1123,7 +1123,7 @@ and have 2 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   sm
 
@@ -1235,7 +1235,7 @@ and have 2 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   sm
 
@@ -1295,7 +1295,7 @@ and have 2 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   sm
 
@@ -1379,7 +1379,7 @@ cat > expect << EOF
 ;   (use "git pull" to merge the remote branch into yours)
 ;
 ; Changes to be committed:
-;   (use "git restore --index <file>..." to unstage)
+;   (use "git restore --staged <file>..." to unstage)
 ;
 ;	modified:   sm
 ;
@@ -1458,7 +1458,7 @@ and have 2 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   sm
 
@@ -1581,7 +1581,7 @@ and have 2 and 2 different commits each, respectively.
   (use "git pull" to merge the remote branch into yours)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   sm
 
diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh
index 6e678456bc..1b9712c675 100755
--- a/t/t7512-status-help.sh
+++ b/t/t7512-status-help.sh
@@ -85,7 +85,7 @@ You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
   (use "git rebase --abort" to check out the original branch)
 
 Unmerged paths:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
   (use "git add <file>..." to mark resolution)
 
 	both modified:   main.txt
@@ -110,7 +110,7 @@ You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
   (all conflicts fixed: run "git rebase --continue")
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   main.txt
 
@@ -148,7 +148,7 @@ You are currently rebasing branch '\''rebase_i_conflicts_second'\'' on '\''$ONTO
   (use "git rebase --abort" to check out the original branch)
 
 Unmerged paths:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
   (use "git add <file>..." to mark resolution)
 
 	both modified:   main.txt
@@ -176,7 +176,7 @@ You are currently rebasing branch '\''rebase_i_conflicts_second'\'' on '\''$ONTO
   (all conflicts fixed: run "git rebase --continue")
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   main.txt
 
@@ -816,7 +816,7 @@ You are currently reverting commit $TO_REVERT.
   (use "git revert --abort" to cancel the revert operation)
 
 Unmerged paths:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
   (use "git add <file>..." to mark resolution)
 
 	both modified:   to-revert.txt
@@ -837,7 +837,7 @@ You are currently reverting commit $TO_REVERT.
   (use "git revert --abort" to cancel the revert operation)
 
 Changes to be committed:
-  (use "git restore --index <file>..." to unstage)
+  (use "git restore --staged <file>..." to unstage)
 
 	modified:   to-revert.txt
 
diff --git a/wt-status.c b/wt-status.c
index 3098e77d72..4d065ce89e 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -181,13 +181,13 @@ static void wt_longstatus_print_unmerged_header(struct wt_status *s)
 	else if (!s->is_initial) {
 		if (!strcmp(s->reference, "HEAD"))
 			status_printf_ln(s, c,
-					 _("  (use \"git restore --index <file>...\" to unstage)"));
+					 _("  (use \"git restore --staged <file>...\" to unstage)"));
 		else
 			status_printf_ln(s, c,
-					 _("  (use \"git restore --source=%s --index <file>...\" to unstage)"),
+					 _("  (use \"git restore --source=%s --staged <file>...\" to unstage)"),
 					 s->reference);
 	} else
-		status_printf_ln(s, c, _("  (use \"git rm --cached <file>...\" to unstage)"));
+		status_printf_ln(s, c, _("  (use \"git rm --staged <file>...\" to unstage)"));
 
 	if (!both_deleted) {
 		if (!del_mod_conflict)
@@ -214,13 +214,13 @@ static void wt_longstatus_print_cached_header(struct wt_status *s)
 	else if (!s->is_initial) {
 		if (!strcmp(s->reference, "HEAD"))
 			status_printf_ln(s, c
-					 , _("  (use \"git restore --index <file>...\" to unstage)"));
+					 , _("  (use \"git restore --staged <file>...\" to unstage)"));
 		else
 			status_printf_ln(s, c,
-					 _("  (use \"git restore --source=%s --index <file>...\" to unstage)"),
+					 _("  (use \"git restore --source=%s --staged <file>...\" to unstage)"),
 					 s->reference);
 	} else
-		status_printf_ln(s, c, _("  (use \"git rm --cached <file>...\" to unstage)"));
+		status_printf_ln(s, c, _("  (use \"git rm --staged <file>...\" to unstage)"));
 	status_printf_ln(s, c, "%s", "");
 }

-- 
2.21.0.682.g30d2204636