diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt
index d271842608..67864c6bbc 100644
--- a/Documentation/git-read-tree.txt
+++ b/Documentation/git-read-tree.txt
@@ -40,12 +40,18 @@ OPTIONS
 --reset::
 	Same as -m, except that unmerged entries are discarded instead
 	of failing. When used with `-u`, updates leading to loss of
-	working tree changes will not abort the operation.
+	working tree changes will not abort the operation and
+	untracked files will be overwritten. Use `--protect-untracked`
+	to avoid overwriting untracked files.
 
 -u::
 	After a successful merge, update the files in the work
 	tree with the result of the merge.
 
+--protect-untracked::
+	Prevent `--reset` `-u` from overwriting untracked files. Requires
+	`--reset` and `-u` (`-m` never overwrites untracked files).
+
 -i::
 	Usually a merge requires the index file as well as the
 	files in the working tree to be up to date with the
@@ -89,7 +95,7 @@ OPTIONS
 	existed in the original index file.
 
 --exclude-per-directory=<gitignore>::
-	When running the command with `-u` and `-m` options, the
+	When using `-u` with `-m` or `--reset` `--protect-untracked` options, the
 	merge result may need to overwrite paths that are not
 	tracked in the current branch.  The command usually
 	refuses to proceed with the merge to avoid losing such a
diff --git a/builtin/am.c b/builtin/am.c
index 912d9821b1..a92394b682 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -1854,7 +1854,8 @@ static void am_resolve(struct am_state *state)
  * true, any unmerged entries will be discarded. Returns 0 on success, -1 on
  * failure.
  */
-static int fast_forward_to(struct tree *head, struct tree *remote, int reset)
+static int fast_forward_to(struct tree *head, struct tree *remote,
+			   enum unpack_trees_reset_type reset)
 {
 	struct lock_file lock_file = LOCK_INIT;
 	struct unpack_trees_options opts;
@@ -1942,7 +1943,8 @@ static int clean_index(const struct object_id *head, const struct object_id *rem
 
 	read_cache_unmerged();
 
-	if (fast_forward_to(head_tree, head_tree, 1))
+	if (fast_forward_to(head_tree, head_tree,
+			    UNPACK_RESET_OVERWRITE_UNTRACKED))
 		return -1;
 
 	if (write_cache_as_tree(&index, 0, NULL))
@@ -1952,7 +1954,7 @@ static int clean_index(const struct object_id *head, const struct object_id *rem
 	if (!index_tree)
 		return error(_("Could not parse object '%s'."), oid_to_hex(&index));
 
-	if (fast_forward_to(index_tree, remote_tree, 0))
+	if (fast_forward_to(index_tree, remote_tree, UNPACK_NO_RESET))
 		return -1;
 
 	if (merge_tree(remote_tree))
diff --git a/builtin/checkout.c b/builtin/checkout.c
index ffa776c6e1..e9e70018f9 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -506,7 +506,7 @@ static int reset_tree(struct tree *tree, const struct checkout_opts *o,
 	opts.head_idx = -1;
 	opts.update = worktree;
 	opts.skip_unmerged = !worktree;
-	opts.reset = 1;
+	opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
 	opts.merge = 1;
 	opts.fn = oneway_merge;
 	opts.verbose_update = o->show_progress;
diff --git a/builtin/read-tree.c b/builtin/read-tree.c
index 5c9c082595..23735adde9 100644
--- a/builtin/read-tree.c
+++ b/builtin/read-tree.c
@@ -114,6 +114,7 @@ static int git_read_tree_config(const char *var, const char *value, void *cb)
 int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
 {
 	int i, stage = 0;
+	int protect_untracked = -1;
 	struct object_id oid;
 	struct tree_desc t[MAX_UNPACK_TREES];
 	struct unpack_trees_options opts;
@@ -140,6 +141,8 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
 		  PARSE_OPT_NONEG },
 		OPT_BOOL('u', NULL, &opts.update,
 			 N_("update working tree with merge result")),
+		OPT_BOOL(0, "protect-untracked", &protect_untracked,
+			 N_("do not overwrite untracked files")),
 		{ OPTION_CALLBACK, 0, "exclude-per-directory", &opts,
 		  N_("gitignore"),
 		  N_("allow explicitly ignored files to be overwritten"),
@@ -209,8 +212,17 @@ int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
 	if ((opts.update || opts.index_only) && !opts.merge)
 		die("%s is meaningless without -m, --reset, or --prefix",
 		    opts.update ? "-u" : "-i");
+	if (protect_untracked >= 0) {
+		if (!opts.reset || !opts.update)
+			die("-%s-protect-untracked requires --reset and -u",
+			    protect_untracked ? "" : "-no");
+		opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
+	}
 	if ((opts.dir && !opts.update))
 		die("--exclude-per-directory is meaningless unless -u");
+	if (opts.dir && opts.reset == UNPACK_RESET_OVERWRITE_UNTRACKED)
+		warning("--exclude-per-directory without --preserve-untracked "
+			"has no effect");
 	if (opts.merge && !opts.index_only)
 		setup_work_tree();
 
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 2e41ad5644..feb30a71f5 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -398,7 +398,7 @@ static int reset_head(struct object_id *oid, const char *action,
 	unpack_tree_opts.update = 1;
 	unpack_tree_opts.merge = 1;
 	if (!detach_head)
-		unpack_tree_opts.reset = 1;
+		unpack_tree_opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
 
 	if (repo_read_index_unmerged(the_repository) < 0) {
 		ret = error(_("could not read index"));
diff --git a/builtin/reset.c b/builtin/reset.c
index 26ef9a7bd0..a39dd92fb2 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -70,7 +70,7 @@ static int reset_index(const struct object_id *oid, int reset_type, int quiet)
 		opts.update = 1;
 		/* fallthrough */
 	default:
-		opts.reset = 1;
+		opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED;
 	}
 
 	read_cache_unmerged();
diff --git a/builtin/stash.c b/builtin/stash.c
index 2a8e6d09b4..175d1b62d3 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -227,7 +227,8 @@ static int clear_stash(int argc, const char **argv, const char *prefix)
 	return do_clear_stash();
 }
 
-static int reset_tree(struct object_id *i_tree, int update, int reset)
+static int reset_tree(struct object_id *i_tree, int update,
+		      enum unpack_trees_reset_type reset)
 {
 	int nr_trees = 1;
 	struct unpack_trees_options opts;
@@ -461,7 +462,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
 	}
 
 	if (has_index) {
-		if (reset_tree(&index_tree, 0, 0))
+		if (reset_tree(&index_tree, 0, UNPACK_NO_RESET))
 			return -1;
 	} else {
 		struct strbuf out = STRBUF_INIT;
@@ -471,7 +472,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
 			return -1;
 		}
 
-		if (reset_tree(&c_tree, 0, 1)) {
+		if (reset_tree(&c_tree, 0, UNPACK_RESET_OVERWRITE_UNTRACKED)) {
 			strbuf_release(&out);
 			return -1;
 		}
diff --git a/t/lib-read-tree.sh b/t/lib-read-tree.sh
index b95f485606..22f516d91f 100644
--- a/t/lib-read-tree.sh
+++ b/t/lib-read-tree.sh
@@ -39,3 +39,14 @@ read_tree_u_must_fail () {
 	test_cmp pre-dry-run-wt post-dry-run-wt &&
 	test_must_fail git read-tree "$@"
 }
+
+read_tree_u_must_fail_save_err () {
+	git ls-files -s >pre-dry-run &&
+	git diff-files -p >pre-dry-run-wt &&
+	test_must_fail git read-tree -n "$@" &&
+	git ls-files -s >post-dry-run &&
+	git diff-files -p >post-dry-run-wt &&
+	test_cmp pre-dry-run post-dry-run &&
+	test_cmp pre-dry-run-wt post-dry-run-wt &&
+	test_must_fail git read-tree "$@" 2>actual-err
+}
diff --git a/t/t1005-read-tree-reset.sh b/t/t1005-read-tree-reset.sh
index 83b09e1310..6c9dd6805b 100755
--- a/t/t1005-read-tree-reset.sh
+++ b/t/t1005-read-tree-reset.sh
@@ -19,15 +19,48 @@ test_expect_success 'setup' '
 	git add df &&
 	echo content >new &&
 	git add new &&
-	git commit -m two
+	git commit -m two &&
+	git ls-files >expect-two
 '
 
-test_expect_success 'reset should work' '
+test_expect_success '--protect-untracked option sanity checks' '
+	read_tree_u_must_fail --reset --protect-untracked HEAD &&
+	read_tree_u_must_fail --reset --no-protect-untracked HEAD &&
+	read_tree_u_must_fail -m -u --protect-untracked HEAD &&
+	read_tree_u_must_fail -m -u --no-protect-untracked
+'
+
+test_expect_success 'reset should reset worktree' '
+	echo changed >df &&
 	read_tree_u_must_succeed -u --reset HEAD^ &&
 	git ls-files >actual &&
 	test_cmp expect actual
 '
 
+test_expect_success 'reset --protect-untracked protects untracked file' '
+	echo changed >new &&
+	read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
+	echo "error: Untracked working tree file '\'new\'' would be overwritten by merge." >expected-err &&
+	test_cmp expected-err actual-err
+'
+
+test_expect_success 'reset --protect-untracked protects untracked directory' '
+	rm new &&
+	mkdir new &&
+	echo untracked >new/untracked &&
+	read_tree_u_must_fail_save_err -u --reset --protect-untracked HEAD &&
+	echo "error: Updating '\'new\'' would lose untracked files in it" >expected-err &&
+	test_cmp expected-err actual-err
+'
+
+test_expect_success 'reset --protect-untracked resets' '
+	rm -rf new &&
+	echo changed >df/file &&
+	read_tree_u_must_succeed -u --reset --protect-untracked HEAD &&
+	git ls-files >actual-two &&
+	test_cmp expect-two actual-two
+'
+
 test_expect_success 'reset should remove remnants from a failed merge' '
 	read_tree_u_must_succeed --reset -u HEAD &&
 	git ls-files -s >expect &&
diff --git a/unpack-trees.c b/unpack-trees.c
index 50189909b8..b1722730fe 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -1917,7 +1917,8 @@ static int verify_absent_1(const struct cache_entry *ce,
 	int len;
 	struct stat st;
 
-	if (o->index_only || o->reset || !o->update)
+	if (o->index_only || o->reset == UNPACK_RESET_OVERWRITE_UNTRACKED ||
+	    !o->update)
 		return 0;
 
 	len = check_leading_path(ce->name, ce_namelen(ce));
diff --git a/unpack-trees.h b/unpack-trees.h
index d344d7d296..732b262c4d 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -41,9 +41,15 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
  */
 void clear_unpack_trees_porcelain(struct unpack_trees_options *opts);
 
+enum unpack_trees_reset_type {
+	UNPACK_NO_RESET = 0,
+	UNPACK_RESET_OVERWRITE_UNTRACKED,
+	UNPACK_RESET_PROTECT_UNTRACKED
+};
+
 struct unpack_trees_options {
-	unsigned int reset,
-		     merge,
+	enum unpack_trees_reset_type reset;
+	unsigned int merge,
 		     update,
 		     clone,
 		     index_only,
