diff mbox

[v2,0/4] Be nicer to the user on tracked/untracked merge conflicts

Message ID 20220527195545.33984-1-git.jonathan.bressat@gmail.com (mailing list archive)
State New, archived
Headers show

Commit Message

Jonathan Bressat May 27, 2022, 7:55 p.m. UTC
We improve our last patch with adding an option to merge and pull command and a
configuration variable.
This make the user able to modify the behavior of merge when it meet untracked files
that have the same content than files in merged branch.

We kept the old behavior as default.

thanks

Jonathan Bressat and 
Guillaume Cogoni

Jonathan Bressat (4):
  t6436: tests how merge behave when there is untracked file with the
    same content
  merge with untracked file that are the same without failure
  add configuration variable corresponding to --overwrite-same-content
  error message now advice to use the new option

 Documentation/config/merge.txt |  5 ++
 Documentation/git-merge.txt    |  4 ++
 builtin/checkout.c             |  1 +
 builtin/merge.c                |  9 +++-
 builtin/pull.c                 |  8 +++-
 cache.h                        |  3 +-
 merge-ort.c                    |  1 +
 merge-recursive.h              |  1 +
 merge.c                        |  4 +-
 sequencer.c                    |  2 +-
 t/t6436-merge-overwrite.sh     | 34 ++++++++++++++
 t/t7615-merge-untracked.sh     | 84 ++++++++++++++++++++++++++++++++++
 unpack-trees.c                 | 27 +++++++++--
 unpack-trees.h                 |  2 +
 14 files changed, 174 insertions(+), 11 deletions(-)
 create mode 100755 t/t7615-merge-untracked.sh

Interdiff contre v1 :

Comments

Matthieu Moy June 4, 2022, 9:44 a.m. UTC | #1
On 5/27/22 21:55, Jonathan Bressat wrote:
> add test to show explicitly that merge doesn't overwrite untracked files
> or unstaged even when they have the same content than files int the
> merged commit

Nit: capital at the beginning of the sentence, period at the end.

"untracked files or unstaged" -> "untracked or unstaged files"

> +test_expect_success 'create branch A' '
> +	git reset --hard c0 &&
> +	git checkout -b A
> +'
> +
> +test_expect_success 'fastforward will not overwrite untracked file with the same content' '

Git usually spells fast-forward with a hyphen, not fastforward.

> +	test_when_finished "git branch -D B && git reset --hard c0 && git clean --force" &&
> +	git checkout -b B &&
> +	test_commit --no-tag "tracked" file "content" &&
> +	git checkout A &&
> +	echo content >file &&
> +	test_must_fail git merge B

Other tests in the same file test a bit more: the file mustn't be 
touched. It's a very important thing with Git: 99% of the times, when an 
operation fails, it fails before starting any change on-disk, as opposed 
to "I started messing up with your repo, I can't go further, go fix the 
mess yourself" ;-).

The way it's done is by creating a file with the content, using "cp" 
instead of "echo >" and "test_cmp" to check the content.

Other tests also check the absence of .git/MERGE_HEAD, which seems to be 
a sensible thing to do.

> +test_expect_success 'will not overwrite untracked file with the same content' '
> +	test_when_finished "git branch -D B && git reset --hard c0 && git clean --force" &&
> +	git checkout -b B &&
> +	test_commit --no-tag "tracked" file "content" fileB "content" &&
> +	git checkout A &&
> +	test_commit --no-tag "exA" fileA "content" &&
> +	echo content >file &&
> +	test_must_fail git merge B
> +'
> +
> +test_expect_success 'will not overwrite unstaged file with the same content' '
> +	test_when_finished "git branch -D B && git reset --hard c0 && git clean --force" &&
> +	test_commit --no-tag "unstaged" file "other" &&
> +	git checkout -b B &&
> +	test_commit --no-tag "staged" file "content" &&
> +	git checkout A &&
> +	echo content >file &&
> +	test_must_fail git merge B
> +'

As discussed IRL, I think two more cases should be tested:

- index matches commit being merged, but the worktree file doesn't
- worktree file doesn't match content, but index does

in both cases, I'd expect the old and the new behavior to abort the 
merge. Perhaps there are use-cases where one would expect a successful 
merge silently, but for rare corner-cases, it's safe to ask the user to 
fix the situation manually and too much magic can only confuse the user.
Matthieu Moy June 4, 2022, 9:45 a.m. UTC | #2
On 5/27/22 21:55, Jonathan Bressat wrote:
> Keep the old behavior as default.
> 
> Add the option --overwrite-same-content, when this option is used merge
> will overwrite untracked file that have the same content.
> 
> It make the merge nicer to the user, usefull for a simple utilisation,

make_s_

usefull -> useful

utilisation -> use

> for exemple if you copy and paste files from another project and then

ex_a_mple.

> you decide to pull this project, git will not proceed even if you didn't
> modify those files.

I'd avoid saying "you" in a commit message. "the user" seems clearer to me.

Also, don't use the future to talk about the behavior before the patch, 
it's really confusing. Actually, the commit message talks about the 
previous behavior, but doesn't really document the new one.

> +--overwrite-same-content::
> +       Silently overwrite untracked files that have the same content
> +       and name than files in the merged commit from the merge result.

I don't understand what "in the merged commit from the merge result" means.

Perhaps "overwrite" is not the best name. We actually re-use the file 
without touching it.

> --- /dev/null
> +++ b/t/t7615-merge-untracked.sh

Why a new file? These are minor variants of the ones you just added in 
the previous commit, and would deserve being written next to them.

> +test_expect_success 'fastforward overwrite untracked file that has the same content' '
> +test_expect_success 'fastforward fail when untracked file has different content' '
> +test_expect_success 'normal merge overwrite untracked file that has the same content' '
> +test_expect_success 'normal merge fail when untracked file has different content' '
> +test_expect_success 'merge fail when tracked file modification is unstaged' '

We're making a lot of tests, very similar to each other and very similar 
to other existing ones. I think we've reached the point where we need to 
refactor a bit and write one generic function that covers

- index state : same / different
- worktree state : same / different
- --overwrite-untracked : present / absent
- kind of merge : fast-forward / real merge

and then call this function with the appropriate set of parameters. 
Either the function can be called within tests (each test becoming a 
one-liner), or perhaps the function can call test_expect_success and 
then we can write stg like

for index in same different
do
	for worktree in same different
	do
	...
		run_test_merge $index $worktree ....
	done
done

> --- a/unpack-trees.c
> +++ b/unpack-trees.c
> @@ -2257,6 +2257,10 @@ static int check_ok_to_remove(const char *name, int len, int dtype,
>   	if (result) {
>   		if (result->ce_flags & CE_REMOVE)
>   			return 0;
> +	} else if (ce && !ie_modified(o->src_index, ce, st, 0)) {
> +		if(o->overwrite_same_content) {
> +			return 0;
> +		}

This looks good, but honestly I'm a bit lost between o->src_index, 
o->dst_index and o.result, so the review of someone more familiar with 
this part of the codebase would be welcome.

> + * is not tracked, unless it is ignored or it has the same content
> + * than the merged file with the option --overwrite_same_content.

"same content _as_".
Matthieu Moy June 4, 2022, 9:45 a.m. UTC | #3
On 5/27/22 21:55, Jonathan Bressat wrote:
 > Subject: Re: [PATCH v2 4/4] error message now advice to use the new 
option

Commit messages are usually written with imperative tone. The "now" 
doesn't add information, the reader can guess that the commit message 
describes the new behavior.

"suggest --overwrite-same-content in error message when appropriate" ?

> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -760,6 +760,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
>   				       &new_branch_info->commit->object.oid :
>   				       &new_branch_info->oid, NULL);
>   		topts.preserve_ignored = !opts->overwrite_ignore;
> +		topts.overwrite_same_content = 0;/* FIXME: opts->overwrite_same_content */

Why not use opts->overwrite_same_content in the code rather than saying 
you should in a comment?

Actually, doesn't this hunk belong to the previous commit?

The rest of the patch looks good to me.
Matthieu Moy June 4, 2022, 9:49 a.m. UTC | #4
On 5/27/22 21:55, Jonathan Bressat wrote:
> Configuration variable merge.overwritesamecontent corresponding to

We usually write them as camelCase for readability (but options are 
case-insensitive).

> +merge.overwritesamecontent::
> +    When set to true, it will modify the behavior of git merge

"When set to true, modify", the future is not needed.

> +    to overwrite untracked files that have the same name and
> +    content than files in the merged commit.	

The doc should mention the command-line option too, and say which one 
takes precedence when both are specified (as it is the case for many 
other options).
Guillaume Cogoni June 10, 2022, 12:58 p.m. UTC | #5
Hello,

Thanks for your reviews.
We will take in consideration what you say for the next version.

Sincerely,
Guillaume COGONI and Jonathan BRESSAT

On Sat, Jun 4, 2022 at 4:57 PM Matthieu Moy <Matthieu.Moy@univ-lyon1.fr> wrote:
>
> On 5/27/22 21:55, Jonathan Bressat wrote:
>  > Subject: Re: [PATCH v2 4/4] error message now advice to use the new
> option
>
> Commit messages are usually written with imperative tone. The "now"
> doesn't add information, the reader can guess that the commit message
> describes the new behavior.
>
> "suggest --overwrite-same-content in error message when appropriate" ?
>
> > --- a/builtin/checkout.c
> > +++ b/builtin/checkout.c
> > @@ -760,6 +760,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
> >                                      &new_branch_info->commit->object.oid :
> >                                      &new_branch_info->oid, NULL);
> >               topts.preserve_ignored = !opts->overwrite_ignore;
> > +             topts.overwrite_same_content = 0;/* FIXME: opts->overwrite_same_content */
>
> Why not use opts->overwrite_same_content in the code rather than saying
> you should in a comment?
>
> Actually, doesn't this hunk belong to the previous commit?
>
> The rest of the patch looks good to me.
>
> --
> Matthieu Moy
> https://matthieu-moy.fr/
diff mbox

Patch

diff --git a/Documentation/config/merge.txt b/Documentation/config/merge.txt
index 99e83dd36e..2824dd19c7 100644
--- a/Documentation/config/merge.txt
+++ b/Documentation/config/merge.txt
@@ -89,6 +89,11 @@  merge.autoStash::
 	`--autostash` options of linkgit:git-merge[1].
 	Defaults to false.
 
+merge.overwritesamecontent::
+    When set to true, it will modify the behavior of git merge 
+    to overwrite untracked files that have the same name and 
+    content than files in the merged commit.	
+
 merge.tool::
 	Controls which merge tool is used by linkgit:git-mergetool[1].
 	The list below shows the valid built-in values.
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index 3125473cc1..ceda0271c2 100644
--- a/Documentation/git-merge.txt
+++ b/Documentation/git-merge.txt
@@ -100,6 +100,10 @@  will be appended to the specified message.
 	Silently overwrite ignored files from the merge result. This
 	is the default behavior. Use `--no-overwrite-ignore` to abort.
 
+--overwrite-same-content::
+       Silently overwrite untracked files that have the same content 
+       and name than files in the merged commit from the merge result.
+
 --abort::
 	Abort the current conflict resolution process, and
 	try to reconstruct the pre-merge state. If an autostash entry is
diff --git a/builtin/checkout.c b/builtin/checkout.c
index cc804ba8e1..1b1d1813c7 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -760,6 +760,7 @@  static int merge_working_tree(const struct checkout_opts *opts,
 				       &new_branch_info->commit->object.oid :
 				       &new_branch_info->oid, NULL);
 		topts.preserve_ignored = !opts->overwrite_ignore;
+		topts.overwrite_same_content = 0;/* FIXME: opts->overwrite_same_content */
 		tree = parse_tree_indirect(old_branch_info->commit ?
 					   &old_branch_info->commit->object.oid :
 					   the_hash_algo->empty_tree);
diff --git a/builtin/merge.c b/builtin/merge.c
index 74e53cf20a..936cb8480d 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -68,6 +68,7 @@  static int option_edit = -1;
 static int allow_trivial = 1, have_message, verify_signatures;
 static int check_trust_level = 1;
 static int overwrite_ignore = 1;
+static int overwrite_same_content;
 static struct strbuf merge_msg = STRBUF_INIT;
 static struct strategy **use_strategies;
 static size_t use_strategies_nr, use_strategies_alloc;
@@ -305,6 +306,7 @@  static struct option builtin_merge_options[] = {
 	OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")),
 	OPT_BOOL(0, "signoff", &signoff, N_("add a Signed-off-by trailer")),
 	OPT_BOOL(0, "no-verify", &no_verify, N_("bypass pre-merge-commit and commit-msg hooks")),
+	OPT_BOOL(0, "overwrite-same-content", &overwrite_same_content, N_("overwrite untracked file with the same content and name")),
 	OPT_END()
 };
 
@@ -656,6 +658,8 @@  static int git_merge_config(const char *k, const char *v, void *cb)
 	} else if (!strcmp(k, "merge.autostash")) {
 		autostash = git_config_bool(k, v);
 		return 0;
+	} else if (!strcmp(k,"merge.overwritesamecontent")) {
+		overwrite_same_content = git_config_bool(k, v);
 	}
 
 	status = fmt_merge_msg_config(k, v, cb);
@@ -684,6 +688,7 @@  static int read_tree_trivial(struct object_id *common, struct object_id *head,
 	opts.trivial_merges_only = 1;
 	opts.merge = 1;
 	opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
+	opts.overwrite_same_content = overwrite_same_content;
 	trees[nr_trees] = parse_tree_indirect(common);
 	if (!trees[nr_trees++])
 		return -1;
@@ -746,6 +751,7 @@  static int try_merge_strategy(const char *strategy, struct commit_list *common,
 
 		o.branch1 = head_arg;
 		o.branch2 = merge_remote_util(remoteheads->item)->name;
+		o.overwrite_same_content = overwrite_same_content;
 
 		for (j = common; j; j = j->next)
 			commit_list_insert(j->item, &reversed);
@@ -1573,7 +1579,8 @@  int cmd_merge(int argc, const char **argv, const char *prefix)
 		if (checkout_fast_forward(the_repository,
 					  &head_commit->object.oid,
 					  &commit->object.oid,
-					  overwrite_ignore)) {
+					  overwrite_ignore,
+					  overwrite_same_content)) {
 			apply_autostash(git_path_merge_autostash(the_repository));
 			ret = 1;
 			goto done;
diff --git a/builtin/pull.c b/builtin/pull.c
index 100cbf9fb8..46ef68e721 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -92,6 +92,7 @@  static struct strvec opt_strategies = STRVEC_INIT;
 static struct strvec opt_strategy_opts = STRVEC_INIT;
 static char *opt_gpg_sign;
 static int opt_allow_unrelated_histories;
+static int opt_overwrite_same_content;
 
 /* Options passed to git-fetch */
 static char *opt_all;
@@ -182,6 +183,7 @@  static struct option pull_options[] = {
 	OPT_SET_INT(0, "allow-unrelated-histories",
 		    &opt_allow_unrelated_histories,
 		    N_("allow merging unrelated histories"), 1),
+	OPT_BOOL(0, "overwrite-same-content", &opt_overwrite_same_content, N_("overwrite untracked file with the same content and name")),
 
 	/* Options passed to git-fetch */
 	OPT_GROUP(N_("Options related to fetching")),
@@ -612,7 +614,7 @@  static int pull_into_void(const struct object_id *merge_head,
 	 */
 	if (checkout_fast_forward(the_repository,
 				  the_hash_algo->empty_tree,
-				  merge_head, 0))
+				  merge_head, 0, opt_overwrite_same_content))
 		return 1;
 
 	if (update_ref("initial pull", "HEAD", merge_head, curr_head, 0, UPDATE_REFS_DIE_ON_ERR))
@@ -679,6 +681,8 @@  static int run_merge(void)
 		strvec_pushf(&args, "--cleanup=%s", cleanup_arg);
 	if (opt_ff)
 		strvec_push(&args, opt_ff);
+	if (opt_overwrite_same_content)
+		strvec_push(&args, "--overwrite-same-content");
 	if (opt_verify)
 		strvec_push(&args, opt_verify);
 	if (opt_verify_signatures)
@@ -1078,7 +1082,7 @@  int cmd_pull(int argc, const char **argv, const char *prefix)
 			"commit %s."), oid_to_hex(&orig_head));
 
 		if (checkout_fast_forward(the_repository, &orig_head,
-					  &curr_head, 0))
+					  &curr_head, 0, opt_overwrite_same_content))
 			die(_("Cannot fast-forward your working tree.\n"
 				"After making sure that you saved anything precious from\n"
 				"$ git diff %s\n"
diff --git a/cache.h b/cache.h
index 281f00ab1b..163367ab41 100644
--- a/cache.h
+++ b/cache.h
@@ -1858,7 +1858,8 @@  int try_merge_command(struct repository *r,
 int checkout_fast_forward(struct repository *r,
 			  const struct object_id *from,
 			  const struct object_id *to,
-			  int overwrite_ignore);
+			  int overwrite_ignore,
+			  int overwrite_same_content);
 
 
 int sane_execvp(const char *file, char *const argv[]);
diff --git a/merge-ort.c b/merge-ort.c
index c319797021..a8d1496b4a 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -4066,6 +4066,7 @@  static int checkout(struct merge_options *opt,
 	unpack_opts.verbose_update = (opt->verbosity > 2);
 	unpack_opts.fn = twoway_merge;
 	unpack_opts.preserve_ignored = 0; /* FIXME: !opts->overwrite_ignore */
+	unpack_opts.overwrite_same_content = opt->overwrite_same_content;
 	parse_tree(prev);
 	init_tree_desc(&trees[0], prev->buffer, prev->size);
 	parse_tree(next);
diff --git a/merge-recursive.h b/merge-recursive.h
index 0795a1d3ec..6f8c5678e0 100644
--- a/merge-recursive.h
+++ b/merge-recursive.h
@@ -46,6 +46,7 @@  struct merge_options {
 	/* miscellaneous control options */
 	const char *subtree_shift;
 	unsigned renormalize : 1;
+	int overwrite_same_content;
 
 	/* internal fields used by the implementation */
 	struct merge_options_internal *priv;
diff --git a/merge.c b/merge.c
index 2382ff66d3..410c92d235 100644
--- a/merge.c
+++ b/merge.c
@@ -47,7 +47,8 @@  int try_merge_command(struct repository *r,
 int checkout_fast_forward(struct repository *r,
 			  const struct object_id *head,
 			  const struct object_id *remote,
-			  int overwrite_ignore)
+			  int overwrite_ignore,
+			  int overwrite_same_content)
 {
 	struct tree *trees[MAX_UNPACK_TREES];
 	struct unpack_trees_options opts;
@@ -80,6 +81,7 @@  int checkout_fast_forward(struct repository *r,
 
 	memset(&opts, 0, sizeof(opts));
 	opts.preserve_ignored = !overwrite_ignore;
+	opts.overwrite_same_content = overwrite_same_content;
 
 	opts.head_idx = 1;
 	opts.src_index = r->index;
diff --git a/sequencer.c b/sequencer.c
index 5213d16e97..d11802c542 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -529,7 +529,7 @@  static int fast_forward_to(struct repository *r,
 	struct strbuf err = STRBUF_INIT;
 
 	repo_read_index(r);
-	if (checkout_fast_forward(r, from, to, 1))
+	if (checkout_fast_forward(r, from, to, 1, 0))
 		return -1; /* the callee should have complained already */
 
 	strbuf_addf(&sb, _("%s: fast-forward"), _(action_name(opts)));
diff --git a/t/t6436-merge-overwrite.sh b/t/t6436-merge-overwrite.sh
index c0b7bd7c3f..bb323b1ee3 100755
--- a/t/t6436-merge-overwrite.sh
+++ b/t/t6436-merge-overwrite.sh
@@ -204,4 +204,38 @@  test_expect_success 'will not clobber WT/index when merging into unborn' '
 	grep bar untracked-file
 '
 
+test_expect_success 'create branch A' '
+	git reset --hard c0 &&
+	git checkout -b A
+'
+
+test_expect_success 'fastforward will not overwrite untracked file with the same content' '
+	test_when_finished "git branch -D B && git reset --hard c0 && git clean --force" &&
+	git checkout -b B &&
+	test_commit --no-tag "tracked" file "content" &&
+	git checkout A &&
+	echo content >file &&
+	test_must_fail git merge B
+'
+
+test_expect_success 'will not overwrite untracked file with the same content' '
+	test_when_finished "git branch -D B && git reset --hard c0 && git clean --force" &&
+	git checkout -b B &&
+	test_commit --no-tag "tracked" file "content" fileB "content" &&
+	git checkout A &&
+	test_commit --no-tag "exA" fileA "content" &&
+	echo content >file &&
+	test_must_fail git merge B
+'
+
+test_expect_success 'will not overwrite unstaged file with the same content' '
+	test_when_finished "git branch -D B && git reset --hard c0 && git clean --force" &&
+	test_commit --no-tag "unstaged" file "other" &&
+	git checkout -b B &&
+	test_commit --no-tag "staged" file "content" &&
+	git checkout A &&
+	echo content >file &&
+	test_must_fail git merge B
+'
+
 test_done
diff --git a/t/t7615-merge-untracked.sh b/t/t7615-merge-untracked.sh
index 99f8bae4c0..cfefd8f473 100755
--- a/t/t7615-merge-untracked.sh
+++ b/t/t7615-merge-untracked.sh
@@ -1,6 +1,6 @@ 
 #!/bin/sh
 
-test_description='test when merge with untracked file'
+test_description='test when merge with untracked files and the option --overwrite-same-content'
 
 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
@@ -18,7 +18,7 @@  test_expect_success 'fastforward overwrite untracked file that has the same cont
 	test_commit --no-tag "tracked" file "content" &&
 	git checkout A &&
 	echo content >file &&
-	git merge B
+	git merge --overwrite-same-content B
 '
 
 test_expect_success 'fastforward fail when untracked file has different content' '
@@ -27,7 +27,7 @@  test_expect_success 'fastforward fail when untracked file has different content'
 	test_commit --no-tag "tracked" file "content" &&
 	git switch A &&
 	echo other >file &&
-	test_must_fail git merge B
+	test_must_fail git merge --overwrite-same-content B
 '
 
 test_expect_success 'normal merge overwrite untracked file that has the same content' '
@@ -37,7 +37,7 @@  test_expect_success 'normal merge overwrite untracked file that has the same con
 	git switch A &&
 	test_commit --no-tag "exA" fileA "content" &&
 	echo content >file &&
-	git merge B
+	git merge --overwrite-same-content B
 '
 
 test_expect_success 'normal merge fail when untracked file has different content' '
@@ -47,7 +47,7 @@  test_expect_success 'normal merge fail when untracked file has different content
 	git switch A &&
 	test_commit --no-tag "exA" fileA "content" &&
 	echo dif >file &&
-	test_must_fail git merge B
+	test_must_fail git merge --overwrite-same-content B
 '
 
 test_expect_success 'merge fail when tracked file modification is unstaged' '
@@ -57,7 +57,28 @@  test_expect_success 'merge fail when tracked file modification is unstaged' '
 	test_commit --no-tag "staged" file "content" &&
 	git switch A &&
 	echo content >file &&
-	test_must_fail git merge B
+	test_must_fail git merge --overwrite-same-content B
+'
+
+test_expect_success 'fastforward overwrite untracked file that has the same content with the configuration variable' '
+	test_when_finished "git branch -D B && git reset --hard init && git clean --force" &&
+	test_config merge.overwritesamecontent true &&
+	git checkout -b B &&
+	test_commit --no-tag "tracked" file "content" &&
+	git checkout A &&
+	echo content >file &&
+	git merge B
+'
+
+test_expect_success 'normal merge overwrite untracked file that has the same content with the configuration variable' '
+	test_when_finished "git branch -D B && git reset --hard init && git clean --force" &&
+	test_config merge.overwritesamecontent true &&
+	git checkout -b B &&
+	test_commit --no-tag "tracked" file "content" fileB "content" &&
+	git switch A &&
+	test_commit --no-tag "exA" fileA "content" &&
+	echo content >file &&
+	git merge B
 '
 
 test_done
diff --git a/unpack-trees.c b/unpack-trees.c
index 61e06c04be..6c660b084b 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -158,17 +158,17 @@  void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
 	if (!strcmp(cmd, "checkout"))
 		msg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)
 		      ? _("The following untracked working tree files would be overwritten by checkout:\n%%s"
-			  "Please move or remove them before you switch branches.")
+			  "Please move or remove them before you switch branches.%%s")
 		      : _("The following untracked working tree files would be overwritten by checkout:\n%%s");
 	else if (!strcmp(cmd, "merge"))
 		msg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)
 		      ? _("The following untracked working tree files would be overwritten by merge:\n%%s"
-			  "Please move or remove them before you merge.")
+			  "Please move or remove them before you merge.%%s")
 		      : _("The following untracked working tree files would be overwritten by merge:\n%%s");
 	else
 		msg = advice_enabled(ADVICE_COMMIT_BEFORE_MERGE)
 		      ? _("The following untracked working tree files would be overwritten by %s:\n%%s"
-			  "Please move or remove them before you %s.")
+			  "Please move or remove them before you %s.%%s")
 		      : _("The following untracked working tree files would be overwritten by %s:\n%%s");
 	msgs[ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN] =
 		strvec_pushf(&opts->msgs_to_free, msg, cmd, cmd);
@@ -251,6 +251,14 @@  static void display_error_msgs(struct unpack_trees_options *o)
 {
 	int e;
 	unsigned error_displayed = 0;
+	const char *can_overwrite_msg;
+
+	if (o->can_overwrite) {
+		can_overwrite_msg = _("\nYou can also rerun the command with --overwrite-same-content to overwrite files with same content.");
+	} else {
+		can_overwrite_msg = "";
+	}
+
 	for (e = 0; e < NB_UNPACK_TREES_ERROR_TYPES; e++) {
 		struct string_list *rejects = &o->unpack_rejects[e];
 
@@ -261,7 +269,8 @@  static void display_error_msgs(struct unpack_trees_options *o)
 			error_displayed = 1;
 			for (i = 0; i < rejects->nr; i++)
 				strbuf_addf(&path, "\t%s\n", rejects->items[i].string);
-			error(ERRORMSG(o, e), super_prefixed(path.buf));
+
+			error(ERRORMSG(o, e), super_prefixed(path.buf), can_overwrite_msg);
 			strbuf_release(&path);
 		}
 		string_list_clear(rejects, 0);
@@ -1711,6 +1720,7 @@  int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
 	struct pattern_list pl;
 	int free_pattern_list = 0;
 	struct dir_struct dir = DIR_INIT;
+	o->can_overwrite = 1;
 
 	if (o->reset == UNPACK_RESET_INVALID)
 		BUG("o->reset had a value of 1; should be UNPACK_TREES_*_UNTRACKED");
@@ -2257,8 +2267,12 @@  static int check_ok_to_remove(const char *name, int len, int dtype,
 	if (result) {
 		if (result->ce_flags & CE_REMOVE)
 			return 0;
-	} else if (!ie_modified(&o->result, ce, st, 0)) {
-		return 0;
+	} else if (ce && !ie_modified(o->src_index, ce, st, 0)) {
+		if(o->overwrite_same_content) {
+			return 0;
+		}
+	} else {
+		o->can_overwrite = 0;
 	}
 
 	return add_rejected_path(o, error_type, name);
@@ -2266,8 +2280,8 @@  static int check_ok_to_remove(const char *name, int len, int dtype,
 
 /*
  * We do not want to remove or overwrite a working tree file that
- * is not tracked, unless it is ignored and unless it has the same
- * content than the merged file.
+ * is not tracked, unless it is ignored or it has the same content
+ * than the merged file with the option --overwrite_same_content.
  */
 static int verify_absent_1(const struct cache_entry *ce,
 			   enum unpack_trees_error_types error_type,
diff --git a/unpack-trees.h b/unpack-trees.h
index efb9edfbb2..2be74ce5bf 100644
--- a/unpack-trees.h
+++ b/unpack-trees.h
@@ -71,6 +71,8 @@  struct unpack_trees_options {
 		     quiet,
 		     exiting_early,
 		     show_all_errors,
+		     overwrite_same_content,
+		     can_overwrite,
 		     dry_run;
 	enum unpack_trees_reset_type reset;
 	const char *prefix;