[PATCH] builtin/ls-files.c:add git ls-file --dedup option

Subsystems: the rest

STALE2016d

34 messages, 4 authors, 2021-01-24 · open the first message on its own page

[PATCH] builtin/ls-files.c:add git ls-file --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-06 08:54:02

From: ZheNing Hu <redacted>

1.When we use git ls-files with both -m -d,
we would find that repeated path,sometimes
it is confusing.
2.When we are performing a branch merge,
 the default git ls-files will also output
 multiple repeated file names.
Therefore, I added the --dedup option to git ls-files.
1. It can be achieved that only the deleted file name
is displayed when using -m, -d, and --dedup at the same time.
2. Add --dedup when merging branches to remove duplicate file
 names. (unless -s, -u are used)

Signed-off-by: ZheNing Hu <redacted>
---
    builtin/ls-files.c:add git ls-file --dedup option
    
    I am reading the source code of git ls-files and learned that git ls
    -files may have duplicate entries when conflict occurs in a branch merge
    or when different options are used at the same time. Users may fell
    confuse when they see these duplicate entries.
    
    As Junio C Hamano said ,it have odd behaviour.
    
    Therefore, we can provide an additional option to git ls-files to delete
    those repeated information.
    
    This fixes https://github.com/gitgitgadget/git/issues/198
    
    Thanks!

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/832

 builtin/ls-files.c | 43 ++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 38 insertions(+), 5 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..66a7e251a46 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int delete_dup;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -301,6 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 {
 	int i;
 	struct strbuf fullname = STRBUF_INIT;
+	const struct cache_entry *last_stage=NULL;
 
 	/* For cached/deleted files we don't need to even do the readdir */
 	if (show_others || show_killed) {
@@ -315,7 +317,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 	if (show_cached || show_stage) {
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
-
+			if(show_cached && delete_dup){
+				switch (ce_stage(ce)) {
+				case 0:
+				default:
+					break;
+				case 1:
+				case 2:
+				case 3:
+					if (last_stage &&
+					!strcmp(last_stage->name, ce->name))
+						continue;
+					last_stage=ce;
+				}
+			}
 			construct_fullname(&fullname, repo, ce);
 
 			if ((dir->flags & DIR_SHOW_IGNORED) &&
@@ -336,7 +351,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			const struct cache_entry *ce = repo->index->cache[i];
 			struct stat st;
 			int err;
-
+			if(delete_dup){
+				switch (ce_stage(ce)) {
+				case 0:
+				default:
+					break;
+				case 1:
+				case 2:
+				case 3:
+					if (last_stage &&
+					!strcmp(last_stage->name, ce->name))
+						continue;
+					last_stage=ce;
+				}
+			}
 			construct_fullname(&fullname, repo, ce);
 
 			if ((dir->flags & DIR_SHOW_IGNORED) &&
@@ -347,10 +375,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			if (ce_skip_worktree(ce))
 				continue;
 			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
+			if(delete_dup && show_deleted && show_modified && err)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			else{
+				if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified && ie_modified(repo->index, ce, &st, 0))
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			}
 		}
 	}
 
@@ -578,6 +610,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0, "dedup", &delete_dup, N_("delete duplicate entry in index")),
 		OPT_END()
 	};
 
base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
-- 
gitgitgadget

Re: [PATCH] builtin/ls-files.c:add git ls-file --dedup option

From: Eric Sunshine <hidden>
Date: 2021-01-07 06:11:19

On Wed, Jan 6, 2021 at 3:54 AM 阿德烈 via GitGitGadget
[off-list ref] wrote:
[...]
Therefore, I added the --dedup option to git ls-files.
1. It can be achieved that only the deleted file name
is displayed when using -m, -d, and --dedup at the same time.
2. Add --dedup when merging branches to remove duplicate file
 names. (unless -s, -u are used)
I'm just pointing out a few minor style issues below; I'm not properly
reviewing the patch...
Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 43 ++++++++++++++++++++++++++++++++++++++-----
1 file changed, 38 insertions(+), 5 deletions(-)
This change adds a new command-line option, so the documentation
(Documentation/git-ls-files.txt) should be updated and at least one
new test should be added (in one of the t/t30??-ls-files-*.sh scripts
probably).
quoted hunk
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
@@ -301,6 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
        struct strbuf fullname = STRBUF_INIT;
+       const struct cache_entry *last_stage=NULL;
Add spaces around `=` similar to the preceding line:

    const struct cache_entry *last_stage = NULL;
quoted hunk
@@ -315,7 +317,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
                for (i = 0; i < repo->index->cache_nr; i++) {
                        const struct cache_entry *ce = repo->index->cache[i];
-
This patch deletes the blank line but this project usually prefers to
have a blank line after declarations.
+                       if(show_cached && delete_dup){
Add space after `if` and before `{`:

    if (show_cached && delete_dup) {
quoted hunk
@@ -336,7 +351,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
+                       if(delete_dup){
Style: if (delete_dup) {
quoted hunk
@@ -347,10 +375,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
-                       if (show_deleted && err)
+                       if(delete_dup && show_deleted && show_modified && err)
Style: if (delete_dup && ...
-                       if (show_modified && ie_modified(repo->index, ce, &st, 0))
-                               show_ce(repo, dir, ce, fullname.buf, tag_modified);
+                       else{
Style: else {
+                               if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
Add space before `/* comment */`.
Add space in "...it, so...".
quoted hunk
@@ -578,6 +610,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
+               OPT_BOOL(0, "dedup", &delete_dup, N_("delete duplicate entry in index")),
The short help makes it seem like it's modifying the index. Perhaps instead:

    N_("suppress duplicate entries")

[PATCH v2 0/2] builtin/ls-files.c:add git ls-file --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-08 14:38:04

I am reading the source code of git ls-files and learned that git ls -files
may have duplicate entries when conflict occurs in a branch merge or when
different options are used at the same time. Users may fell confuse when
they see these duplicate entries.

As Junio C Hamano said ,it have odd behaviour.

Therefore, we can provide an additional option to git ls-files to delete
those repeated information.

This fixes https://github.com/gitgitgadget/git/issues/198

Thanks!

ZheNing Hu (2):
  builtin/ls-files.c:add git ls-file --dedup option
  builtin:ls-files.c:add git ls-file --dedup option

 Documentation/git-ls-files.txt |  4 +++
 builtin/ls-files.c             | 41 ++++++++++++++++++++--
 t/t3012-ls-files-dedup.sh      | 63 ++++++++++++++++++++++++++++++++++
 3 files changed, 105 insertions(+), 3 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh


base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/832

Range-diff vs v1:

 1:  0261e5d245e = 1:  0261e5d245e builtin/ls-files.c:add git ls-file --dedup option
 -:  ----------- > 2:  a09a5098aa6 builtin:ls-files.c:add git ls-file --dedup option

-- 
gitgitgadget

[PATCH v2 1/2] builtin/ls-files.c:add git ls-file --dedup option

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-08 14:38:04

From: ZheNing Hu <redacted>

1.When we use git ls-files with both -m -d,
we would find that repeated path,sometimes
it is confusing.
2.When we are performing a branch merge,
 the default git ls-files will also output
 multiple repeated file names.
Therefore, I added the --dedup option to git ls-files.
1. It can be achieved that only the deleted file name
is displayed when using -m, -d, and --dedup at the same time.
2. Add --dedup when merging branches to remove duplicate file
 names. (unless -s, -u are used)

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 43 ++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 38 insertions(+), 5 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..66a7e251a46 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int delete_dup;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -301,6 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 {
 	int i;
 	struct strbuf fullname = STRBUF_INIT;
+	const struct cache_entry *last_stage=NULL;
 
 	/* For cached/deleted files we don't need to even do the readdir */
 	if (show_others || show_killed) {
@@ -315,7 +317,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 	if (show_cached || show_stage) {
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
-
+			if(show_cached && delete_dup){
+				switch (ce_stage(ce)) {
+				case 0:
+				default:
+					break;
+				case 1:
+				case 2:
+				case 3:
+					if (last_stage &&
+					!strcmp(last_stage->name, ce->name))
+						continue;
+					last_stage=ce;
+				}
+			}
 			construct_fullname(&fullname, repo, ce);
 
 			if ((dir->flags & DIR_SHOW_IGNORED) &&
@@ -336,7 +351,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			const struct cache_entry *ce = repo->index->cache[i];
 			struct stat st;
 			int err;
-
+			if(delete_dup){
+				switch (ce_stage(ce)) {
+				case 0:
+				default:
+					break;
+				case 1:
+				case 2:
+				case 3:
+					if (last_stage &&
+					!strcmp(last_stage->name, ce->name))
+						continue;
+					last_stage=ce;
+				}
+			}
 			construct_fullname(&fullname, repo, ce);
 
 			if ((dir->flags & DIR_SHOW_IGNORED) &&
@@ -347,10 +375,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			if (ce_skip_worktree(ce))
 				continue;
 			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
+			if(delete_dup && show_deleted && show_modified && err)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			else{
+				if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified && ie_modified(repo->index, ce, &st, 0))
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			}
 		}
 	}
 
@@ -578,6 +610,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0, "dedup", &delete_dup, N_("delete duplicate entry in index")),
 		OPT_END()
 	};
 
-- 
gitgitgadget

[PATCH v2 2/2] builtin:ls-files.c:add git ls-file --dedup option

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-08 14:38:04

From: ZheNing Hu <redacted>

This commit standardizes the code format.
For git ls-file --dedup option added
relevant descriptions in Documentation/git-ls-files.txt
and wrote t/t3012-ls-files-dedup.sh test script
to prove the correctness of--dedup option.

this patch fixed: https://github.com/gitgitgadget/git/issues/198
Thanks.

Signed-off-by: ZheNing Hu <redacted>
---
 Documentation/git-ls-files.txt |  4 +++
 builtin/ls-files.c             | 20 ++++++-----
 t/t3012-ls-files-dedup.sh      | 63 ++++++++++++++++++++++++++++++++++
 3 files changed, 78 insertions(+), 9 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index cbcf5263dd0..41a9c5a8b27 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--dedup]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -81,6 +82,9 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--dedup::
+	Suppress duplicates entries when conflicts happen or
+	specify -d -m at the same time.
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 66a7e251a46..bc4eded19ab 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -302,7 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 {
 	int i;
 	struct strbuf fullname = STRBUF_INIT;
-	const struct cache_entry *last_stage=NULL;
+	const struct cache_entry *last_stage = NULL;
 
 	/* For cached/deleted files we don't need to even do the readdir */
 	if (show_others || show_killed) {
@@ -317,7 +317,8 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 	if (show_cached || show_stage) {
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
-			if(show_cached && delete_dup){
+
+			if (show_cached && delete_dup) {
 				switch (ce_stage(ce)) {
 				case 0:
 				default:
@@ -328,7 +329,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 					if (last_stage &&
 					!strcmp(last_stage->name, ce->name))
 						continue;
-					last_stage=ce;
+					last_stage = ce;
 				}
 			}
 			construct_fullname(&fullname, repo, ce);
@@ -351,7 +352,8 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			const struct cache_entry *ce = repo->index->cache[i];
 			struct stat st;
 			int err;
-			if(delete_dup){
+
+			if (delete_dup) {
 				switch (ce_stage(ce)) {
 				case 0:
 				default:
@@ -362,7 +364,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 					if (last_stage &&
 					!strcmp(last_stage->name, ce->name))
 						continue;
-					last_stage=ce;
+					last_stage = ce;
 				}
 			}
 			construct_fullname(&fullname, repo, ce);
@@ -375,10 +377,10 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			if (ce_skip_worktree(ce))
 				continue;
 			err = lstat(fullname.buf, &st);
-			if(delete_dup && show_deleted && show_modified && err)
+			if (delete_dup && show_deleted && show_modified && err)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			else{
-				if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
+			else {
+				if (show_deleted && err)
 					show_ce(repo, dir, ce, fullname.buf, tag_removed);
 				if (show_modified && ie_modified(repo->index, ce, &st, 0))
 					show_ce(repo, dir, ce, fullname.buf, tag_modified);
@@ -610,7 +612,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
-		OPT_BOOL(0, "dedup", &delete_dup, N_("delete duplicate entry in index")),
+		OPT_BOOL(0, "dedup", &delete_dup, N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 00000000000..00c7f65cfc1
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='git ls-files --dedup test.
+
+This test prepares the following in the cache:
+
+    a.txt       - a file(base)
+    a.txt	- a file(master)
+    a.txt       - a file(dev)
+    b.txt       - a file
+    delete.txt  - a file
+    expect1	- a file
+    expect2	- a file
+
+'
+
+. ./test-lib.sh
+
+test_expect_success 'master branch setup and write expect1 expect2 and commit' '
+	touch a.txt &&
+	touch b.txt &&
+	touch delete.txt &&
+	cat <<-EOF >expect1 &&
+	M a.txt
+	H b.txt
+	H delete.txt
+	H expect1
+	H expect2
+	EOF
+	cat <<-EOF >expect2 &&
+	C a.txt
+	R delete.txt
+	EOF
+	git add a.txt b.txt delete.txt expect1 expect2 &&
+	git commit -m master:1
+'
+
+test_expect_success 'main commit again' '
+	echo a>a.txt &&
+	echo b>b.txt &&
+	echo delete>delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m master:2
+'
+
+test_expect_success 'dev commit' '
+	git checkout HEAD~ &&
+	git switch -c dev &&
+	echo change>a.txt &&
+	git add a.txt &&
+	git commit -m dev:1
+'
+
+test_expect_success 'dev merge master' '
+	test_must_fail git merge master &&
+	git ls-files -t --dedup >actual1 &&
+	test_cmp expect1 actual1 &&
+	rm delete.txt &&
+	git ls-files -d -m -t --dedup >actual2 &&
+	test_cmp expect2 actual2
+'
+
+test_done
-- 
gitgitgadget

Re: [PATCH v2 2/2] builtin:ls-files.c:add git ls-file --dedup option

From: Eric Sunshine <hidden>
Date: 2021-01-14 06:40:08

On Fri, Jan 8, 2021 at 9:36 AM ZheNing Hu via GitGitGadget
[off-list ref] wrote:
builtin:ls-files.c:add git ls-file --dedup option
This subject concisely explains the purpose of the patch. That's good.
A more typical way to write it would be:

    ls-files: add --dedup option
This commit standardizes the code format.
Fixing problems pointed out by reviewers is good. Normally, however,
when you submit a new version of your patch or patch series, you
should apply these fixes directly to the patch(es) which introduced
the problems in the first place rather than adding one or more
additional patches to fix problems introduced in earlier patches. To
do this, you typically would use `git rebase -i` or `git commit
--amend` to squash the fixes into the problematic patches. Thus, when
you re-submit the patches, they will appear to be "perfect".

For this particular two-patch series, patch [2/2] is doing two things:
(1) fixing style problems from patch [1/2], and (2) adding
documentation and tests which logically belong with the feature added
by patch [1/2]. Taking the above advice into account, a better
presentation when you re-submit this series would be to squash these
two patches into a single patch.
quoted hunk
Signed-off-by: ZheNing Hu <redacted>
---
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
@@ -81,6 +82,9 @@ OPTIONS
+--dedup::
+       Suppress duplicates entries when conflicts happen or
+       specify -d -m at the same time.
For consistency with typesetting elsewhere in this file, use backticks
around the command-line options. It also often is a good idea to spell
the options using long form since it is typically easier to search for
the long form of an option in documentation. So, perhaps the above can
be written like this:

    Suppress duplicate entries when `--deleted` and `--modified` are
    combined.
quoted hunk
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
-       const struct cache_entry *last_stage=NULL;
+       const struct cache_entry *last_stage = NULL;
-                       if(show_cached && delete_dup){
+                       if (show_cached && delete_dup) {
-                                       last_stage=ce;
+                                       last_stage = ce;
-                       if(delete_dup){
+                       if (delete_dup) {
-                       if(delete_dup && show_deleted && show_modified && err)
+                       if (delete_dup && show_deleted && show_modified && err)
-                       else{
-                               if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
+                       else {
+                               if (show_deleted && err)
As mentioned above, these style fixes should be squashed into the
first patch, rather than being done in a separate patch, so that
reviewers see a nicely polished patch rather than a patch which
requires later fixing up.
quoted hunk
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,63 @@
+test_expect_success 'master branch setup and write expect1 expect2 and commit' '
We usually give this test a simple title such as "setup" so that we
don't have to worry about the title becoming outdated as people make
changes to the test itself.
+       touch a.txt &&
+       touch b.txt &&
+       touch delete.txt &&
On this project, we use `touch` when the timestamp of the empty files
is important to the test. If the timestamp is not important, then we
just use `>`, like this:

    >a.txt &&
    >b.txt &&
    >delete.txt &&
+       cat <<-EOF >expect1 &&
+       M a.txt
+       H b.txt
+       H delete.txt
+       H expect1
+       H expect2
+       EOF
+       cat <<-EOF >expect2 &&
+       C a.txt
+       R delete.txt
+       EOF
When no variables are being interpolated in the here-doc content, we
use -\EOF to let readers know that the here-doc body is literal. So:

    cat >expect1 <<-\EOF &&
    ...
    EOF
+       git add a.txt b.txt delete.txt expect1 expect2 &&
+       git commit -m master:1
+'
+
+test_expect_success 'main commit again' '
+       echo a>a.txt &&
+       echo b>b.txt &&
+       echo delete>delete.txt &&
+       git add a.txt b.txt delete.txt &&
+       git commit -m master:2
+'
+
+test_expect_success 'dev commit' '
+       git checkout HEAD~ &&
+       git switch -c dev &&
+       echo change>a.txt &&
+       git add a.txt &&
+       git commit -m dev:1
+'
These two tests following the "setup" test also seem to be doing setup
tasks rather than testing the new --dedup functionality. If this is
the case, then it probably would make sense to combine all three tests
into a single "setup" test.
+test_expect_success 'dev merge master' '
+       test_must_fail git merge master &&
+       git ls-files -t --dedup >actual1 &&
+       test_cmp expect1 actual1 &&
+       rm delete.txt &&
+       git ls-files -d -m -t --dedup >actual2 &&
+       test_cmp expect2 actual2
+'
Do you foresee that people will add more tests to this file which will
use the files and branches set up by the "setup" test(s)? If not, if
those branches and files are only ever going to be used by this one
test, then it probably would be better to combine all the above code
into a single test.

Re: [PATCH v2 2/2] builtin:ls-files.c:add git ls-file --dedup option

From: 胡哲宁 <hidden>
Date: 2021-01-14 08:16:39

You can see that the coding and documentation of GIT community are really very
standard, which may be one of the things I lack and need to improve ;)
Thanks for patiently correct my errors.

Eric Sunshine [off-list ref] 于2021年1月14日周四 下午2:39写道:
On Fri, Jan 8, 2021 at 9:36 AM ZheNing Hu via GitGitGadget
[off-list ref] wrote:
quoted
builtin:ls-files.c:add git ls-file --dedup option
This subject concisely explains the purpose of the patch. That's good.
A more typical way to write it would be:

    ls-files: add --dedup option
OK.I will correct it more specification.
quoted
This commit standardizes the code format.
Fixing problems pointed out by reviewers is good. Normally, however,
when you submit a new version of your patch or patch series, you
should apply these fixes directly to the patch(es) which introduced
the problems in the first place rather than adding one or more
additional patches to fix problems introduced in earlier patches. To
do this, you typically would use `git rebase -i` or `git commit
--amend` to squash the fixes into the problematic patches. Thus, when
you re-submit the patches, they will appear to be "perfect".

For this particular two-patch series, patch [2/2] is doing two things:
(1) fixing style problems from patch [1/2], and (2) adding
documentation and tests which logically belong with the feature added
by patch [1/2]. Taking the above advice into account, a better
presentation when you re-submit this series would be to squash these
two patches into a single patch.
I thought before this was gitgitgadget would sent duplicate patch
over and over again. It seems like I really should go straight ahead
and squash my commits , so I know what I should do.
quoted
Signed-off-by: ZheNing Hu <redacted>
---
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
@@ -81,6 +82,9 @@ OPTIONS
+--dedup::
+       Suppress duplicates entries when conflicts happen or
+       specify -d -m at the same time.
For consistency with typesetting elsewhere in this file, use backticks
around the command-line options. It also often is a good idea to spell
the options using long form since it is typically easier to search for
the long form of an option in documentation. So, perhaps the above can
be written like this:

    Suppress duplicate entries when `--deleted` and `--modified` are
    combined.
quoted
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
-       const struct cache_entry *last_stage=NULL;
+       const struct cache_entry *last_stage = NULL;
-                       if(show_cached && delete_dup){
+                       if (show_cached && delete_dup) {
-                                       last_stage=ce;
+                                       last_stage = ce;
-                       if(delete_dup){
+                       if (delete_dup) {
-                       if(delete_dup && show_deleted && show_modified && err)
+                       if (delete_dup && show_deleted && show_modified && err)
-                       else{
-                               if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
+                       else {
+                               if (show_deleted && err)
As mentioned above, these style fixes should be squashed into the
first patch, rather than being done in a separate patch, so that
reviewers see a nicely polished patch rather than a patch which
requires later fixing up.
quoted
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,63 @@
+test_expect_success 'master branch setup and write expect1 expect2 and commit' '
We usually give this test a simple title such as "setup" so that we
don't have to worry about the title becoming outdated as people make
changes to the test itself.
quoted
+       touch a.txt &&
+       touch b.txt &&
+       touch delete.txt &&
On this project, we use `touch` when the timestamp of the empty files
is important to the test. If the timestamp is not important, then we
just use `>`, like this:

    >a.txt &&
    >b.txt &&
    >delete.txt &&
OK,maybe because I always use touch to generate files.
quoted
+       cat <<-EOF >expect1 &&
+       M a.txt
+       H b.txt
+       H delete.txt
+       H expect1
+       H expect2
+       EOF
+       cat <<-EOF >expect2 &&
+       C a.txt
+       R delete.txt
+       EOF
When no variables are being interpolated in the here-doc content, we
use -\EOF to let readers know that the here-doc body is literal. So:

    cat >expect1 <<-\EOF &&
    ...
    EOF
quoted
+       git add a.txt b.txt delete.txt expect1 expect2 &&
+       git commit -m master:1
+'
+
+test_expect_success 'main commit again' '
+       echo a>a.txt &&
+       echo b>b.txt &&
+       echo delete>delete.txt &&
+       git add a.txt b.txt delete.txt &&
+       git commit -m master:2
+'
+
+test_expect_success 'dev commit' '
+       git checkout HEAD~ &&
+       git switch -c dev &&
+       echo change>a.txt &&
+       git add a.txt &&
+       git commit -m dev:1
+'
These two tests following the "setup" test also seem to be doing setup
tasks rather than testing the new --dedup functionality. If this is
the case, then it probably would make sense to combine all three tests
into a single "setup" test.
quoted
+test_expect_success 'dev merge master' '
+       test_must_fail git merge master &&
+       git ls-files -t --dedup >actual1 &&
+       test_cmp expect1 actual1 &&
+       rm delete.txt &&
+       git ls-files -d -m -t --dedup >actual2 &&
+       test_cmp expect2 actual2
+'
Do you foresee that people will add more tests to this file which will
use the files and branches set up by the "setup" test(s)? If not, if
those branches and files are only ever going to be used by this one
test, then it probably would be better to combine all the above code
into a single test.
No,the test file may just need only one.

[PATCH v3] ls-files.c: add --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-14 12:23:08

From: ZheNing Hu <redacted>

In order to provide users a better experience
when viewing information about files in the index
and the working tree, the `--dedup` option will suppress
some duplicate options under some conditions.

In a merge conflict, one item of "git ls-files" output may
appear multiple times. For example,now the file `a.c` has
a conflict,`a.c` will appear three times in the output of
"git ls-files".We can use "git ls-files --dedup" to output
`a.c` only one time.(unless `--stage` or `--unmerged` is
used to view all the detailed information in the index)

In addition, if you use both `--delete` and `--modify` in
the same time, The `--dedup` option can also suppress modified
entries output.

`--dedup` option relevant descriptions in
`Documentation/git-ls-files.txt`,
the test script in `t/t3012-ls-files-dedup.sh`
prove the correctness of the `--dedup` option.

this patch fixed:
https://github.com/gitgitgadget/git/issues/198
Thanks.

Signed-off-by: ZheNing Hu <redacted>
---
    builtin/ls-files.c:add git ls-file --dedup option
    
    I am reading the source code of git ls-files and learned that git ls
    -files may have duplicate entries when conflict occurs in a branch merge
    or when different options are used at the same time. Users may fell
    confuse when they see these duplicate entries.
    
    As Junio C Hamano said ,it have odd behaviour.
    
    Therefore, we can provide an additional option to git ls-files to delete
    those repeated information.
    
    This fixes https://github.com/gitgitgadget/git/issues/198
    
    Thanks!

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/832

Range-diff vs v2:

 1:  0261e5d245e < -:  ----------- builtin/ls-files.c:add git ls-file --dedup option
 2:  a09a5098aa6 ! 1:  5ce52c8b7a4 builtin:ls-files.c:add git ls-file --dedup option
     @@ Metadata
      Author: ZheNing Hu [off-list ref]
      
       ## Commit message ##
     -    builtin:ls-files.c:add git ls-file --dedup option
     +    ls-files.c: add --dedup option
      
     -    This commit standardizes the code format.
     -    For git ls-file --dedup option added
     -    relevant descriptions in Documentation/git-ls-files.txt
     -    and wrote t/t3012-ls-files-dedup.sh test script
     -    to prove the correctness of--dedup option.
     +    In order to provide users a better experience
     +    when viewing information about files in the index
     +    and the working tree, the `--dedup` option will suppress
     +    some duplicate options under some conditions.
      
     -    this patch fixed: https://github.com/gitgitgadget/git/issues/198
     +    In a merge conflict, one item of "git ls-files" output may
     +    appear multiple times. For example,now the file `a.c` has
     +    a conflict,`a.c` will appear three times in the output of
     +    "git ls-files".We can use "git ls-files --dedup" to output
     +    `a.c` only one time.(unless `--stage` or `--unmerged` is
     +    used to view all the detailed information in the index)
     +
     +    In addition, if you use both `--delete` and `--modify` in
     +    the same time, The `--dedup` option can also suppress modified
     +    entries output.
     +
     +    `--dedup` option relevant descriptions in
     +    `Documentation/git-ls-files.txt`,
     +    the test script in `t/t3012-ls-files-dedup.sh`
     +    prove the correctness of the `--dedup` option.
     +
     +    this patch fixed:
     +    https://github.com/gitgitgadget/git/issues/198
          Thanks.
      
          Signed-off-by: ZheNing Hu [off-list ref]
     @@ Documentation/git-ls-files.txt: OPTIONS
       	See OUTPUT below for more information.
       
      +--dedup::
     -+	Suppress duplicates entries when conflicts happen or
     -+	specify -d -m at the same time.
     ++	Suppress duplicate entries when conflict happen or `--deleted`
     ++	and `--modified` are combined.
     ++
       -x <pattern>::
       --exclude=<pattern>::
       	Skip untracked files matching pattern.
      
       ## builtin/ls-files.c ##
     +@@ builtin/ls-files.c: static int line_terminator = '\n';
     + static int debug_mode;
     + static int show_eol;
     + static int recurse_submodules;
     ++static int delete_dup;
     + 
     + static const char *prefix;
     + static int max_prefix_len;
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
       {
       	int i;
       	struct strbuf fullname = STRBUF_INIT;
     --	const struct cache_entry *last_stage=NULL;
      +	const struct cache_entry *last_stage = NULL;
       
       	/* For cached/deleted files we don't need to even do the readdir */
       	if (show_others || show_killed) {
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 	if (show_cached || show_stage) {
       		for (i = 0; i < repo->index->cache_nr; i++) {
       			const struct cache_entry *ce = repo->index->cache[i];
     --			if(show_cached && delete_dup){
     -+
     + 
      +			if (show_cached && delete_dup) {
     - 				switch (ce_stage(ce)) {
     - 				case 0:
     - 				default:
     -@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 					if (last_stage &&
     - 					!strcmp(last_stage->name, ce->name))
     - 						continue;
     --					last_stage=ce;
     ++				switch (ce_stage(ce)) {
     ++				case 0:
     ++				default:
     ++					break;
     ++				case 1:
     ++				case 2:
     ++				case 3:
     ++					if (last_stage &&
     ++					!strcmp(last_stage->name, ce->name))
     ++						continue;
      +					last_stage = ce;
     - 				}
     - 			}
     ++				}
     ++			}
       			construct_fullname(&fullname, repo, ce);
     + 
     + 			if ((dir->flags & DIR_SHOW_IGNORED) &&
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 			const struct cache_entry *ce = repo->index->cache[i];
       			struct stat st;
       			int err;
     --			if(delete_dup){
     -+
     + 
      +			if (delete_dup) {
     - 				switch (ce_stage(ce)) {
     - 				case 0:
     - 				default:
     -@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 					if (last_stage &&
     - 					!strcmp(last_stage->name, ce->name))
     - 						continue;
     --					last_stage=ce;
     ++				switch (ce_stage(ce)) {
     ++				case 0:
     ++				default:
     ++					break;
     ++				case 1:
     ++				case 2:
     ++				case 3:
     ++					if (last_stage &&
     ++					!strcmp(last_stage->name, ce->name))
     ++						continue;
      +					last_stage = ce;
     - 				}
     - 			}
     ++				}
     ++			}
       			construct_fullname(&fullname, repo, ce);
     + 
     + 			if ((dir->flags & DIR_SHOW_IGNORED) &&
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
       			if (ce_skip_worktree(ce))
       				continue;
       			err = lstat(fullname.buf, &st);
     --			if(delete_dup && show_deleted && show_modified && err)
     +-			if (show_deleted && err)
      +			if (delete_dup && show_deleted && show_modified && err)
       				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     --			else{
     --				if (show_deleted && err)/* you can't find it,so it's actually removed at all! */
     +-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
     +-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
      +			else {
      +				if (show_deleted && err)
     - 					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     - 				if (show_modified && ie_modified(repo->index, ce, &st, 0))
     - 					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++				if (show_modified && ie_modified(repo->index, ce, &st, 0))
     ++					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++			}
     + 		}
     + 	}
     + 
      @@ builtin/ls-files.c: int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
       			N_("pretend that paths removed since <tree-ish> are still present")),
       		OPT__ABBREV(&abbrev),
       		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
     --		OPT_BOOL(0, "dedup", &delete_dup, N_("delete duplicate entry in index")),
      +		OPT_BOOL(0, "dedup", &delete_dup, N_("suppress duplicate entries")),
       		OPT_END()
       	};
     @@ t/t3012-ls-files-dedup.sh (new)
      +
      +. ./test-lib.sh
      +
     -+test_expect_success 'master branch setup and write expect1 expect2 and commit' '
     -+	touch a.txt &&
     -+	touch b.txt &&
     -+	touch delete.txt &&
     -+	cat <<-EOF >expect1 &&
     ++test_expect_success 'setup' '
     ++	> a.txt &&
     ++	> b.txt &&
     ++	> delete.txt &&
     ++	cat >expect1<<-\EOF &&
      +	M a.txt
      +	H b.txt
      +	H delete.txt
      +	H expect1
      +	H expect2
      +	EOF
     -+	cat <<-EOF >expect2 &&
     ++	cat >expect2<<-EOF &&
      +	C a.txt
      +	R delete.txt
      +	EOF
      +	git add a.txt b.txt delete.txt expect1 expect2 &&
     -+	git commit -m master:1
     -+'
     -+
     -+test_expect_success 'main commit again' '
     ++	git commit -m master:1 &&
      +	echo a>a.txt &&
      +	echo b>b.txt &&
     -+	echo delete>delete.txt &&
     ++	echo delete >delete.txt &&
      +	git add a.txt b.txt delete.txt &&
     -+	git commit -m master:2
     -+'
     -+
     -+test_expect_success 'dev commit' '
     ++	git commit -m master:2 &&
      +	git checkout HEAD~ &&
      +	git switch -c dev &&
     -+	echo change>a.txt &&
     ++	echo change >a.txt &&
      +	git add a.txt &&
     -+	git commit -m dev:1
     -+'
     -+
     -+test_expect_success 'dev merge master' '
     ++	git commit -m dev:1 &&
      +	test_must_fail git merge master &&
      +	git ls-files -t --dedup >actual1 &&
      +	test_cmp expect1 actual1 &&


 Documentation/git-ls-files.txt |  5 ++++
 builtin/ls-files.c             | 41 ++++++++++++++++++++++++--
 t/t3012-ls-files-dedup.sh      | 54 ++++++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+), 3 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index cbcf5263dd0..0f8dbeeea20 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--dedup]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -81,6 +82,10 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--dedup::
+	Suppress duplicate entries when conflict happen or `--deleted`
+	and `--modified` are combined.
+
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..bc4eded19ab 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int delete_dup;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -301,6 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 {
 	int i;
 	struct strbuf fullname = STRBUF_INIT;
+	const struct cache_entry *last_stage = NULL;
 
 	/* For cached/deleted files we don't need to even do the readdir */
 	if (show_others || show_killed) {
@@ -316,6 +318,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
 
+			if (show_cached && delete_dup) {
+				switch (ce_stage(ce)) {
+				case 0:
+				default:
+					break;
+				case 1:
+				case 2:
+				case 3:
+					if (last_stage &&
+					!strcmp(last_stage->name, ce->name))
+						continue;
+					last_stage = ce;
+				}
+			}
 			construct_fullname(&fullname, repo, ce);
 
 			if ((dir->flags & DIR_SHOW_IGNORED) &&
@@ -337,6 +353,20 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			struct stat st;
 			int err;
 
+			if (delete_dup) {
+				switch (ce_stage(ce)) {
+				case 0:
+				default:
+					break;
+				case 1:
+				case 2:
+				case 3:
+					if (last_stage &&
+					!strcmp(last_stage->name, ce->name))
+						continue;
+					last_stage = ce;
+				}
+			}
 			construct_fullname(&fullname, repo, ce);
 
 			if ((dir->flags & DIR_SHOW_IGNORED) &&
@@ -347,10 +377,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			if (ce_skip_worktree(ce))
 				continue;
 			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
+			if (delete_dup && show_deleted && show_modified && err)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			else {
+				if (show_deleted && err)
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified && ie_modified(repo->index, ce, &st, 0))
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			}
 		}
 	}
 
@@ -578,6 +612,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0, "dedup", &delete_dup, N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 00000000000..aec7d364235
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+test_description='git ls-files --dedup test.
+
+This test prepares the following in the cache:
+
+    a.txt       - a file(base)
+    a.txt	- a file(master)
+    a.txt       - a file(dev)
+    b.txt       - a file
+    delete.txt  - a file
+    expect1	- a file
+    expect2	- a file
+
+'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	> a.txt &&
+	> b.txt &&
+	> delete.txt &&
+	cat >expect1<<-\EOF &&
+	M a.txt
+	H b.txt
+	H delete.txt
+	H expect1
+	H expect2
+	EOF
+	cat >expect2<<-EOF &&
+	C a.txt
+	R delete.txt
+	EOF
+	git add a.txt b.txt delete.txt expect1 expect2 &&
+	git commit -m master:1 &&
+	echo a>a.txt &&
+	echo b>b.txt &&
+	echo delete >delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m master:2 &&
+	git checkout HEAD~ &&
+	git switch -c dev &&
+	echo change >a.txt &&
+	git add a.txt &&
+	git commit -m dev:1 &&
+	test_must_fail git merge master &&
+	git ls-files -t --dedup >actual1 &&
+	test_cmp expect1 actual1 &&
+	rm delete.txt &&
+	git ls-files -d -m -t --dedup >actual2 &&
+	test_cmp expect2 actual2
+'
+
+test_done
base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
-- 
gitgitgadget

Re: [PATCH v3] ls-files.c: add --dedup option

From: Eric Sunshine <hidden>
Date: 2021-01-16 07:16:58

On Thu, Jan 14, 2021 at 7:22 AM 阿德烈 via GitGitGadget
[off-list ref] wrote:
In order to provide users a better experience
when viewing information about files in the index
and the working tree, the `--dedup` option will suppress
some duplicate options under some conditions.
[...]
I have a few very minor comments alongside Junio's review comments...
quoted hunk
Signed-off-by: ZheNing Hu <redacted>
---
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,54 @@
+test_description='git ls-files --dedup test.
+
+This test prepares the following in the cache:
+
+    a.txt       - a file(base)
+    a.txt      - a file(master)
+    a.txt       - a file(dev)
+    b.txt       - a file
+    delete.txt  - a file
+    expect1    - a file
+    expect2    - a file
+
+'
This test script description is outdated now. Perhaps shorten it to:

    test_description='ls-files dedup tests'

Or, it might be suitable to simply add the new test to the existing
t3004-ls-files-basic.sh instead.
+test_expect_success 'setup' '
+       > a.txt &&
+       > b.txt &&
+       > delete.txt &&
+       cat >expect1<<-\EOF &&
Style nits: no space after redirection operator and a space before
redirection operator:

    >a.txt &&
    >b.txt &&
    >delete.txt &&
    cat >expect1 <<-\EOF &&
+       cat >expect2<<-EOF &&
Nit: missing the backslash (and wrong spacing):

    cat >expect2 <<-\EOF &&
+       echo a>a.txt &&
+       echo b>b.txt &&
Style:

    echo a >a.txt &&
    echo b >b.txt &&
+       echo delete >delete.txt &&
+       git add a.txt b.txt delete.txt &&
+       git commit -m master:2 &&
+       git checkout HEAD~ &&
+       git switch -c dev &&
If someone adds a new test after this test, then that new test will
run in the "dev" branch, which might be unexpected or undesirable. It
often is a good idea to ensure that tests do certain types of cleanup
to avoid breaking subsequent tests. Here, it would be a good idea to
ensure that the test switches back to the original branch when it
finishes (regardless of whether it finishes successfully or
unsuccessfully).

    git switch -c dev &&
    test_when_finished "git switch master" &&

Or you could use `git switch -` if you don't want to hard-code the
name "master" in the test (since there has been effort lately to
remove that name from tests.
+       echo change >a.txt &&
+       git add a.txt &&
+       git commit -m dev:1 &&
+       test_must_fail git merge master &&
+       git ls-files -t --dedup >actual1 &&
+       test_cmp expect1 actual1 &&
+       rm delete.txt &&
+       git ls-files -d -m -t --dedup >actual2 &&
+       test_cmp expect2 actual2
We usually don't bother giving temporary files unique names like
"actual1" and "actual2" unless those files must exist at the same
time. This is because unique names like this may confuse readers into
wondering if there is some hidden interdependency between the files.
In this case, the files don't need to exist at the same time, so it
may be better simply to use the names "actual" and "expect", like
this:

    ...other stuff...
    cat >expect <<-\EOF &&
    ...
    EOF
    git ls-files -t --dedup >actual &&
    test_cmp expect actual &&
    rm delete.txt &&
    cat >expect <<-\EOF &&
    ...
    EOF
    git ls-files -d -m -t --dedup >actual &&
    test_cmp expect actual

(It also has the benefit that the "expect" content is closer to the
place where it is actually used, which may make it a bit easier for a
person reading the test to understand what is supposed to be
produced.)

Re: [PATCH v3] ls-files.c: add --dedup option

From: 胡哲宁 <hidden>
Date: 2021-01-17 03:48:58

Eric,Thanks!
I have little confuse about I can use` test_when_finished "git switch master" `,
but I can't use` test_when_finished "git switch -" `,
why?

Eric Sunshine [off-list ref] 于2021年1月16日周六 下午3:13写道:
On Thu, Jan 14, 2021 at 7:22 AM 阿德烈 via GitGitGadget
[off-list ref] wrote:
quoted
In order to provide users a better experience
when viewing information about files in the index
and the working tree, the `--dedup` option will suppress
some duplicate options under some conditions.
[...]
I have a few very minor comments alongside Junio's review comments...
quoted
Signed-off-by: ZheNing Hu <redacted>
---
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,54 @@
+test_description='git ls-files --dedup test.
+
+This test prepares the following in the cache:
+
+    a.txt       - a file(base)
+    a.txt      - a file(master)
+    a.txt       - a file(dev)
+    b.txt       - a file
+    delete.txt  - a file
+    expect1    - a file
+    expect2    - a file
+
+'
This test script description is outdated now. Perhaps shorten it to:

    test_description='ls-files dedup tests'

Or, it might be suitable to simply add the new test to the existing
t3004-ls-files-basic.sh instead.
quoted
+test_expect_success 'setup' '
+       > a.txt &&
+       > b.txt &&
+       > delete.txt &&
+       cat >expect1<<-\EOF &&
Style nits: no space after redirection operator and a space before
redirection operator:

    >a.txt &&
    >b.txt &&
    >delete.txt &&
    cat >expect1 <<-\EOF &&
quoted
+       cat >expect2<<-EOF &&
Nit: missing the backslash (and wrong spacing):

    cat >expect2 <<-\EOF &&
quoted
+       echo a>a.txt &&
+       echo b>b.txt &&
Style:

    echo a >a.txt &&
    echo b >b.txt &&
quoted
+       echo delete >delete.txt &&
+       git add a.txt b.txt delete.txt &&
+       git commit -m master:2 &&
+       git checkout HEAD~ &&
+       git switch -c dev &&
If someone adds a new test after this test, then that new test will
run in the "dev" branch, which might be unexpected or undesirable. It
often is a good idea to ensure that tests do certain types of cleanup
to avoid breaking subsequent tests. Here, it would be a good idea to
ensure that the test switches back to the original branch when it
finishes (regardless of whether it finishes successfully or
unsuccessfully).

    git switch -c dev &&
    test_when_finished "git switch master" &&

Or you could use `git switch -` if you don't want to hard-code the
name "master" in the test (since there has been effort lately to
remove that name from tests.
quoted
+       echo change >a.txt &&
+       git add a.txt &&
+       git commit -m dev:1 &&
+       test_must_fail git merge master &&
+       git ls-files -t --dedup >actual1 &&
+       test_cmp expect1 actual1 &&
+       rm delete.txt &&
+       git ls-files -d -m -t --dedup >actual2 &&
+       test_cmp expect2 actual2
We usually don't bother giving temporary files unique names like
"actual1" and "actual2" unless those files must exist at the same
time. This is because unique names like this may confuse readers into
wondering if there is some hidden interdependency between the files.
In this case, the files don't need to exist at the same time, so it
may be better simply to use the names "actual" and "expect", like
this:

    ...other stuff...
    cat >expect <<-\EOF &&
    ...
    EOF
    git ls-files -t --dedup >actual &&
    test_cmp expect actual &&
    rm delete.txt &&
    cat >expect <<-\EOF &&
    ...
    EOF
    git ls-files -d -m -t --dedup >actual &&
    test_cmp expect actual

(It also has the benefit that the "expect" content is closer to the
place where it is actually used, which may make it a bit easier for a
person reading the test to understand what is supposed to be
produced.)

Re: [PATCH v3] ls-files.c: add --dedup option

From: Eric Sunshine <hidden>
Date: 2021-01-17 05:12:58

On Sat, Jan 16, 2021 at 10:48 PM 胡哲宁 [off-list ref] wrote:
Eric Sunshine [off-list ref] 于2021年1月16日周六 下午3:13写道:
quoted
quoted
+       git switch -c dev &&
If someone adds a new test after this test, then that new test will
run in the "dev" branch, which might be unexpected or undesirable. It
often is a good idea to ensure that tests do certain types of cleanup
to avoid breaking subsequent tests. Here, it would be a good idea to
ensure that the test switches back to the original branch when it
finishes (regardless of whether it finishes successfully or
unsuccessfully).

    git switch -c dev &&
    test_when_finished "git switch master" &&

Or you could use `git switch -` if you don't want to hard-code the
name "master" in the test (since there has been effort lately to
remove that name from tests.
I have little confuse about I can use` test_when_finished "git switch master" `,
but I can't use` test_when_finished "git switch -" `,
why?
You may use either one. I presented both as alternative approaches.

[PATCH v4 0/3] builtin/ls-files.c:add git ls-file --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-17 04:03:19

I am reading the source code of git ls-files and learned that git ls-files
may have duplicate files name when there are unmerged path in a branch merge
or when different options are used at the same time. Users may fell confuse
when they see these duplicate file names.

As Junio C Hamano said ,it have odd behaviour.

Therefore, we can provide an additional option to git ls-files to delete
those repeated information.

This fixes https://github.com/gitgitgadget/git/issues/198

Thanks!

ZheNing Hu (3):
  ls_files.c: bugfix for --deleted and --modified
  ls_files.c: consolidate two for loops into one
  ls-files: add --deduplicate option

 Documentation/git-ls-files.txt |  5 +++
 builtin/ls-files.c             | 82 +++++++++++++++++++---------------
 t/t3012-ls-files-dedup.sh      | 57 +++++++++++++++++++++++
 3 files changed, 109 insertions(+), 35 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh


base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/832

Range-diff vs v3:

 -:  ----------- > 1:  f4d9af8a312 ls_files.c: bugfix for --deleted and --modified
 -:  ----------- > 2:  50efd9b45b1 ls_files.c: consolidate two for loops into one
 1:  5ce52c8b7a4 ! 3:  0c7830d07db ls-files.c: add --dedup option
     @@ Metadata
      Author: ZheNing Hu [off-list ref]
      
       ## Commit message ##
     -    ls-files.c: add --dedup option
     +    ls-files: add --deduplicate option
      
          In order to provide users a better experience
          when viewing information about files in the index
     -    and the working tree, the `--dedup` option will suppress
     -    some duplicate options under some conditions.
     +    and the working tree, the `--deduplicate` option will suppress
     +    some duplicate name under some conditions.
      
     -    In a merge conflict, one item of "git ls-files" output may
     -    appear multiple times. For example,now the file `a.c` has
     -    a conflict,`a.c` will appear three times in the output of
     -    "git ls-files".We can use "git ls-files --dedup" to output
     +    In a merge conflict, one file name of "git ls-files" output may
     +    appear multiple times. For example,now there is an unmerged path
     +    `a.c`,`a.c` will appear three times in the output of
     +    "git ls-files".We can use "git ls-files --deduplicate" to output
          `a.c` only one time.(unless `--stage` or `--unmerged` is
          used to view all the detailed information in the index)
      
     -    In addition, if you use both `--delete` and `--modify` in
     -    the same time, The `--dedup` option can also suppress modified
     -    entries output.
     +    In addition, if you use both `--delete` and `--modify` at
     +    the same time, The `--deduplicate` option
     +    can also suppress file name output.
      
     -    `--dedup` option relevant descriptions in
     -    `Documentation/git-ls-files.txt`,
     -    the test script in `t/t3012-ls-files-dedup.sh`
     -    prove the correctness of the `--dedup` option.
     -
     -    this patch fixed:
     -    https://github.com/gitgitgadget/git/issues/198
     -    Thanks.
     +    Additional instructions:
     +    In order to display entries information,`deduplicate` suppresses
     +    the output of duplicate file names, not the output of duplicate
     +    entries information, so under the option of `-t`, `--stage`, `--unmerge`,
     +    `--deduplicate` will have no effect.
      
          Signed-off-by: ZheNing Hu [off-list ref]
      
     @@ Documentation/git-ls-files.txt: SYNOPSIS
       		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
       		(-[c|d|o|i|s|u|k|m])*
       		[--eol]
     -+		[--dedup]
     ++		[--deduplicate]
       		[-x <pattern>|--exclude=<pattern>]
       		[-X <file>|--exclude-from=<file>]
       		[--exclude-per-directory=<file>]
     @@ Documentation/git-ls-files.txt: OPTIONS
       	\0 line termination on output and do not quote filenames.
       	See OUTPUT below for more information.
       
     -+--dedup::
     -+	Suppress duplicate entries when conflict happen or `--deleted`
     -+	and `--modified` are combined.
     ++--deduplicate::
     ++	Suppress duplicate entries when there are unmerged paths in index
     ++	or `--deleted` and `--modified` are combined.
      +
       -x <pattern>::
       --exclude=<pattern>::
     @@ builtin/ls-files.c: static int line_terminator = '\n';
       static int debug_mode;
       static int show_eol;
       static int recurse_submodules;
     -+static int delete_dup;
     ++static int skipping_duplicates;
       
       static const char *prefix;
       static int max_prefix_len;
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
       {
       	int i;
       	struct strbuf fullname = STRBUF_INIT;
     -+	const struct cache_entry *last_stage = NULL;
     ++	const struct cache_entry *last_shown_ce;
       
       	/* For cached/deleted files we don't need to even do the readdir */
       	if (show_others || show_killed) {
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 		for (i = 0; i < repo->index->cache_nr; i++) {
     - 			const struct cache_entry *ce = repo->index->cache[i];
     - 
     -+			if (show_cached && delete_dup) {
     -+				switch (ce_stage(ce)) {
     -+				case 0:
     -+				default:
     -+					break;
     -+				case 1:
     -+				case 2:
     -+				case 3:
     -+					if (last_stage &&
     -+					!strcmp(last_stage->name, ce->name))
     -+						continue;
     -+					last_stage = ce;
     -+				}
     -+			}
     - 			construct_fullname(&fullname, repo, ce);
     - 
     - 			if ((dir->flags & DIR_SHOW_IGNORED) &&
     + 	}
     + 	if (! (show_cached || show_stage || show_deleted || show_modified))
     + 		return;
     ++	last_shown_ce = NULL;
     + 	for (i = 0; i < repo->index->cache_nr; i++) {
     + 		const struct cache_entry *ce = repo->index->cache[i];
     + 		struct stat st;
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 			struct stat st;
     - 			int err;
       
     -+			if (delete_dup) {
     -+				switch (ce_stage(ce)) {
     -+				case 0:
     -+				default:
     -+					break;
     -+				case 1:
     -+				case 2:
     -+				case 3:
     -+					if (last_stage &&
     -+					!strcmp(last_stage->name, ce->name))
     -+						continue;
     -+					last_stage = ce;
     -+				}
     -+			}
     - 			construct_fullname(&fullname, repo, ce);
     + 		construct_fullname(&fullname, repo, ce);
       
     - 			if ((dir->flags & DIR_SHOW_IGNORED) &&
     -@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 			if (ce_skip_worktree(ce))
     - 				continue;
     - 			err = lstat(fullname.buf, &st);
     --			if (show_deleted && err)
     -+			if (delete_dup && show_deleted && show_modified && err)
     - 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     --			if (show_modified && ie_modified(repo->index, ce, &st, 0))
     --				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++		if (skipping_duplicates && last_shown_ce &&
     ++			!strcmp(last_shown_ce->name,ce->name))
     ++				continue;
     + 		if ((dir->flags & DIR_SHOW_IGNORED) &&
     + 			!ce_excluded(dir, repo->index, fullname.buf, ce))
     + 			continue;
     + 		if (ce->ce_flags & CE_UPDATE)
     + 			continue;
     + 		if (show_cached || show_stage) {
     ++			if (show_cached && skipping_duplicates && last_shown_ce &&
     ++				!strcmp(last_shown_ce->name,ce->name))
     ++					continue;
     + 			if (!show_unmerged || ce_stage(ce))
     + 				show_ce(repo, dir, ce, fullname.buf,
     + 					ce_stage(ce) ? tag_unmerged :
     + 					(ce_skip_worktree(ce) ? tag_skip_worktree :
     + 						tag_cached));
     ++			if(show_cached && skipping_duplicates)
     ++				last_shown_ce = ce;
     + 		}
     + 		if (ce_skip_worktree(ce))
     + 			continue;
     ++		if (skipping_duplicates && last_shown_ce && !strcmp(last_shown_ce->name,ce->name))
     ++			continue;
     + 		err = lstat(fullname.buf, &st);
     + 		if (err) {
     ++			if (skipping_duplicates && show_deleted && show_modified)
     ++				show_ce(repo, dir, ce, fullname.buf, tag_removed);
      +			else {
     -+				if (show_deleted && err)
     -+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     -+				if (show_modified && ie_modified(repo->index, ce, &st, 0))
     -+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     + 				if (show_deleted)
     + 					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     + 				if (show_modified)
     + 					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     +-		}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
      +			}
     - 		}
     ++		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     + 			show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++		last_shown_ce = ce;
       	}
       
     + 	strbuf_release(&fullname);
      @@ builtin/ls-files.c: int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
       			N_("pretend that paths removed since <tree-ish> are still present")),
       		OPT__ABBREV(&abbrev),
       		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
     -+		OPT_BOOL(0, "dedup", &delete_dup, N_("suppress duplicate entries")),
     ++		OPT_BOOL(0,"deduplicate",&skipping_duplicates,N_("suppress duplicate entries")),
       		OPT_END()
       	};
       
     +@@ builtin/ls-files.c: int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
     + 		tag_skip_worktree = "S ";
     + 		tag_resolve_undo = "U ";
     + 	}
     ++	if (show_tag && skipping_duplicates)
     ++		skipping_duplicates = 0;
     + 	if (show_modified || show_others || show_deleted || (dir.flags & DIR_SHOW_IGNORED) || show_killed)
     + 		require_work_tree = 1;
     + 	if (show_unmerged)
      
       ## t/t3012-ls-files-dedup.sh (new) ##
      @@
      +#!/bin/sh
      +
     -+test_description='git ls-files --dedup test.
     -+
     -+This test prepares the following in the cache:
     -+
     -+    a.txt       - a file(base)
     -+    a.txt	- a file(master)
     -+    a.txt       - a file(dev)
     -+    b.txt       - a file
     -+    delete.txt  - a file
     -+    expect1	- a file
     -+    expect2	- a file
     -+
     -+'
     ++test_description='git ls-files --deduplicate test'
      +
      +. ./test-lib.sh
      +
      +test_expect_success 'setup' '
     -+	> a.txt &&
     -+	> b.txt &&
     -+	> delete.txt &&
     -+	cat >expect1<<-\EOF &&
     -+	M a.txt
     -+	H b.txt
     -+	H delete.txt
     -+	H expect1
     -+	H expect2
     -+	EOF
     -+	cat >expect2<<-EOF &&
     -+	C a.txt
     -+	R delete.txt
     -+	EOF
     -+	git add a.txt b.txt delete.txt expect1 expect2 &&
     ++	>a.txt &&
     ++	>b.txt &&
     ++	>delete.txt &&
     ++	git add a.txt b.txt delete.txt &&
      +	git commit -m master:1 &&
     -+	echo a>a.txt &&
     -+	echo b>b.txt &&
     ++	echo a >a.txt &&
     ++	echo b >b.txt &&
      +	echo delete >delete.txt &&
      +	git add a.txt b.txt delete.txt &&
      +	git commit -m master:2 &&
      +	git checkout HEAD~ &&
      +	git switch -c dev &&
     ++	test_when_finished "git switch master" &&
      +	echo change >a.txt &&
      +	git add a.txt &&
      +	git commit -m dev:1 &&
      +	test_must_fail git merge master &&
     -+	git ls-files -t --dedup >actual1 &&
     -+	test_cmp expect1 actual1 &&
     ++	git ls-files --deduplicate >actual &&
     ++	cat >expect <<-\EOF &&
     ++	a.txt
     ++	b.txt
     ++	delete.txt
     ++	EOF
     ++	test_cmp expect actual &&
      +	rm delete.txt &&
     -+	git ls-files -d -m -t --dedup >actual2 &&
     -+	test_cmp expect2 actual2
     ++	git ls-files -d -m --deduplicate >actual &&
     ++	cat >expect <<-\EOF &&
     ++	a.txt
     ++	delete.txt
     ++	EOF
     ++	test_cmp expect actual &&
     ++	git ls-files -d -m -t  --deduplicate >actual &&
     ++	cat >expect <<-\EOF &&
     ++	C a.txt
     ++	C a.txt
     ++	C a.txt
     ++	R delete.txt
     ++	C delete.txt
     ++	EOF
     ++	test_cmp expect actual &&
     ++	git ls-files -d -m -c  --deduplicate >actual &&
     ++	cat >expect <<-\EOF &&
     ++	a.txt
     ++	b.txt
     ++	delete.txt
     ++	EOF
     ++	test_cmp expect actual &&
     ++	git merge --abort
      +'
     -+
      +test_done

-- 
gitgitgadget

[PATCH v4 1/3] ls_files.c: bugfix for --deleted and --modified

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-17 04:03:19

From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

It's buggy!

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..6f97a23c2dc 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -347,9 +347,12 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			if (ce_skip_worktree(ce))
 				continue;
 			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
-				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			if (err) {
+					if (show_deleted)
+						show_ce(repo, dir, ce, fullname.buf, tag_removed);
+					if (show_modified)
+						show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
 		}
 	}
-- 
gitgitgadget

[PATCH v4 2/3] ls_files.c: consolidate two for loops into one

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-17 04:03:20

From: ZheNing Hu <redacted>

Refactor the two for loops into one,skip showing the ce if it
has the same name as the previously shown one, only when doing so
won't lose information.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 68 +++++++++++++++++++---------------------------
 1 file changed, 28 insertions(+), 40 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 6f97a23c2dc..49c242128d7 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,49 +312,37 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (show_killed)
 			show_killed_files(repo->index, dir);
 	}
-	if (show_cached || show_stage) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-
-			construct_fullname(&fullname, repo, ce);
-
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (show_unmerged && !ce_stage(ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			show_ce(repo, dir, ce, fullname.buf,
-				ce_stage(ce) ? tag_unmerged :
-				(ce_skip_worktree(ce) ? tag_skip_worktree :
-				 tag_cached));
-		}
-	}
-	if (show_deleted || show_modified) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-			struct stat st;
-			int err;
+	if (! (show_cached || show_stage || show_deleted || show_modified))
+		return;
+	for (i = 0; i < repo->index->cache_nr; i++) {
+		const struct cache_entry *ce = repo->index->cache[i];
+		struct stat st;
+		int err;
 
-			construct_fullname(&fullname, repo, ce);
+		construct_fullname(&fullname, repo, ce);
 
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			if (ce_skip_worktree(ce))
-				continue;
-			err = lstat(fullname.buf, &st);
-			if (err) {
-					if (show_deleted)
-						show_ce(repo, dir, ce, fullname.buf, tag_removed);
-					if (show_modified)
-						show_ce(repo, dir, ce, fullname.buf, tag_modified);
-			}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+		if ((dir->flags & DIR_SHOW_IGNORED) &&
+			!ce_excluded(dir, repo->index, fullname.buf, ce))
+			continue;
+		if (ce->ce_flags & CE_UPDATE)
+			continue;
+		if (show_cached || show_stage) {
+			if (!show_unmerged || ce_stage(ce))
+				show_ce(repo, dir, ce, fullname.buf,
+					ce_stage(ce) ? tag_unmerged :
+					(ce_skip_worktree(ce) ? tag_skip_worktree :
+						tag_cached));
 		}
+		if (ce_skip_worktree(ce))
+			continue;
+		err = lstat(fullname.buf, &st);
+		if (err) {
+				if (show_deleted)
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified)
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+		}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			show_ce(repo, dir, ce, fullname.buf, tag_modified);
 	}
 
 	strbuf_release(&fullname);
-- 
gitgitgadget

[PATCH v4 3/3] ls-files: add --deduplicate option

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-17 04:03:20

From: ZheNing Hu <redacted>

In order to provide users a better experience
when viewing information about files in the index
and the working tree, the `--deduplicate` option will suppress
some duplicate name under some conditions.

In a merge conflict, one file name of "git ls-files" output may
appear multiple times. For example,now there is an unmerged path
`a.c`,`a.c` will appear three times in the output of
"git ls-files".We can use "git ls-files --deduplicate" to output
`a.c` only one time.(unless `--stage` or `--unmerged` is
used to view all the detailed information in the index)

In addition, if you use both `--delete` and `--modify` at
the same time, The `--deduplicate` option
can also suppress file name output.

Additional instructions:
In order to display entries information,`deduplicate` suppresses
the output of duplicate file names, not the output of duplicate
entries information, so under the option of `-t`, `--stage`, `--unmerge`,
`--deduplicate` will have no effect.

Signed-off-by: ZheNing Hu <redacted>
---
 Documentation/git-ls-files.txt |  5 +++
 builtin/ls-files.c             | 23 +++++++++++++-
 t/t3012-ls-files-dedup.sh      | 57 ++++++++++++++++++++++++++++++++++
 3 files changed, 84 insertions(+), 1 deletion(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index cbcf5263dd0..d11c8ade402 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--deduplicate]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -81,6 +82,10 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--deduplicate::
+	Suppress duplicate entries when there are unmerged paths in index
+	or `--deleted` and `--modified` are combined.
+
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 49c242128d7..390d7ef6b44 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int skipping_duplicates;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -301,6 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 {
 	int i;
 	struct strbuf fullname = STRBUF_INIT;
+	const struct cache_entry *last_shown_ce;
 
 	/* For cached/deleted files we don't need to even do the readdir */
 	if (show_others || show_killed) {
@@ -314,6 +316,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 	}
 	if (! (show_cached || show_stage || show_deleted || show_modified))
 		return;
+	last_shown_ce = NULL;
 	for (i = 0; i < repo->index->cache_nr; i++) {
 		const struct cache_entry *ce = repo->index->cache[i];
 		struct stat st;
@@ -321,28 +324,43 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 
 		construct_fullname(&fullname, repo, ce);
 
+		if (skipping_duplicates && last_shown_ce &&
+			!strcmp(last_shown_ce->name,ce->name))
+				continue;
 		if ((dir->flags & DIR_SHOW_IGNORED) &&
 			!ce_excluded(dir, repo->index, fullname.buf, ce))
 			continue;
 		if (ce->ce_flags & CE_UPDATE)
 			continue;
 		if (show_cached || show_stage) {
+			if (show_cached && skipping_duplicates && last_shown_ce &&
+				!strcmp(last_shown_ce->name,ce->name))
+					continue;
 			if (!show_unmerged || ce_stage(ce))
 				show_ce(repo, dir, ce, fullname.buf,
 					ce_stage(ce) ? tag_unmerged :
 					(ce_skip_worktree(ce) ? tag_skip_worktree :
 						tag_cached));
+			if(show_cached && skipping_duplicates)
+				last_shown_ce = ce;
 		}
 		if (ce_skip_worktree(ce))
 			continue;
+		if (skipping_duplicates && last_shown_ce && !strcmp(last_shown_ce->name,ce->name))
+			continue;
 		err = lstat(fullname.buf, &st);
 		if (err) {
+			if (skipping_duplicates && show_deleted && show_modified)
+				show_ce(repo, dir, ce, fullname.buf, tag_removed);
+			else {
 				if (show_deleted)
 					show_ce(repo, dir, ce, fullname.buf, tag_removed);
 				if (show_modified)
 					show_ce(repo, dir, ce, fullname.buf, tag_modified);
-		}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			}
+		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
 			show_ce(repo, dir, ce, fullname.buf, tag_modified);
+		last_shown_ce = ce;
 	}
 
 	strbuf_release(&fullname);
@@ -569,6 +587,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0,"deduplicate",&skipping_duplicates,N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
@@ -600,6 +619,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 		tag_skip_worktree = "S ";
 		tag_resolve_undo = "U ";
 	}
+	if (show_tag && skipping_duplicates)
+		skipping_duplicates = 0;
 	if (show_modified || show_others || show_deleted || (dir.flags & DIR_SHOW_IGNORED) || show_killed)
 		require_work_tree = 1;
 	if (show_unmerged)
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 00000000000..75877255c2c
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+test_description='git ls-files --deduplicate test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	>a.txt &&
+	>b.txt &&
+	>delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m master:1 &&
+	echo a >a.txt &&
+	echo b >b.txt &&
+	echo delete >delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m master:2 &&
+	git checkout HEAD~ &&
+	git switch -c dev &&
+	test_when_finished "git switch master" &&
+	echo change >a.txt &&
+	git add a.txt &&
+	git commit -m dev:1 &&
+	test_must_fail git merge master &&
+	git ls-files --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	rm delete.txt &&
+	git ls-files -d -m --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -t  --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	C a.txt
+	C a.txt
+	C a.txt
+	R delete.txt
+	C delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -c  --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+test_done
-- 
gitgitgadget

[PATCH v5 0/3] builtin/ls-files.c:add git ls-file --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-19 06:32:54

I am reading the source code of git ls-files and learned that git ls-files
may have duplicate files name when there are unmerged path in a branch merge
or when different options are used at the same time. Users may fell confuse
when they see these duplicate file names.

As Junio C Hamano said ,it have odd behaviour.

Therefore, we can provide an additional option to git ls-files to delete
those repeated information.

This fixes https://github.com/gitgitgadget/git/issues/198

Thanks!

ZheNing Hu (3):
  ls_files.c: bugfix for --deleted and --modified
  ls_files.c: consolidate two for loops into one
  ls-files.c: add --deduplicate option

 Documentation/git-ls-files.txt |  5 ++
 builtin/ls-files.c             | 83 ++++++++++++++++++++--------------
 t/t3012-ls-files-dedup.sh      | 66 +++++++++++++++++++++++++++
 3 files changed, 120 insertions(+), 34 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh


base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v5
Pull-Request: https://github.com/gitgitgadget/git/pull/832

Range-diff vs v4:

 1:  f4d9af8a312 ! 1:  ec9464f6094 ls_files.c: bugfix for --deleted and --modified
     @@ Commit message
          This situation may occur in the original code: lstat() failed
          but we use `&st` to feed ie_modified() later.
      
     -    It's buggy!
     -
          Therefore, we can directly execute show_ce without the judgment of
          ie_modified() when lstat() has failed.
      
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -				show_ce(repo, dir, ce, fullname.buf, tag_removed);
      -			if (show_modified && ie_modified(repo->index, ce, &st, 0))
      +			if (err) {
     -+					if (show_deleted)
     -+						show_ce(repo, dir, ce, fullname.buf, tag_removed);
     -+					if (show_modified)
     -+						show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+			}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     ++				if (errno != ENOENT && errno != ENOTDIR)
     ++				    error_errno("cannot lstat '%s'", fullname.buf);
     ++				if (show_deleted)
     ++					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++				if (show_modified)
     ++					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
       				show_ce(repo, dir, ce, fullname.buf, tag_modified);
       		}
       	}
 2:  50efd9b45b1 ! 2:  802ff802be8 ls_files.c: consolidate two for loops into one
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -	if (show_cached || show_stage) {
      -		for (i = 0; i < repo->index->cache_nr; i++) {
      -			const struct cache_entry *ce = repo->index->cache[i];
     --
     ++	if (! (show_cached || show_stage || show_deleted || show_modified))
     ++		return;
     ++	for (i = 0; i < repo->index->cache_nr; i++) {
     ++		const struct cache_entry *ce = repo->index->cache[i];
     ++		struct stat st;
     ++		int err;
     + 
      -			construct_fullname(&fullname, repo, ce);
     --
     ++		construct_fullname(&fullname, repo, ce);
     + 
      -			if ((dir->flags & DIR_SHOW_IGNORED) &&
      -			    !ce_excluded(dir, repo->index, fullname.buf, ce))
      -				continue;
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -				ce_stage(ce) ? tag_unmerged :
      -				(ce_skip_worktree(ce) ? tag_skip_worktree :
      -				 tag_cached));
     --		}
     ++		if ((dir->flags & DIR_SHOW_IGNORED) &&
     ++			!ce_excluded(dir, repo->index, fullname.buf, ce))
     ++			continue;
     ++		if (ce->ce_flags & CE_UPDATE)
     ++			continue;
     ++		if (show_cached || show_stage) {
     ++			if (!show_unmerged || ce_stage(ce))
     ++				show_ce(repo, dir, ce, fullname.buf,
     ++					ce_stage(ce) ? tag_unmerged :
     ++					(ce_skip_worktree(ce) ? tag_skip_worktree :
     ++						tag_cached));
     + 		}
      -	}
      -	if (show_deleted || show_modified) {
      -		for (i = 0; i < repo->index->cache_nr; i++) {
      -			const struct cache_entry *ce = repo->index->cache[i];
      -			struct stat st;
      -			int err;
     -+	if (! (show_cached || show_stage || show_deleted || show_modified))
     -+		return;
     -+	for (i = 0; i < repo->index->cache_nr; i++) {
     -+		const struct cache_entry *ce = repo->index->cache[i];
     -+		struct stat st;
     -+		int err;
     - 
     +-
      -			construct_fullname(&fullname, repo, ce);
     -+		construct_fullname(&fullname, repo, ce);
     - 
     +-
      -			if ((dir->flags & DIR_SHOW_IGNORED) &&
      -			    !ce_excluded(dir, repo->index, fullname.buf, ce))
      -				continue;
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -				continue;
      -			err = lstat(fullname.buf, &st);
      -			if (err) {
     --					if (show_deleted)
     --						show_ce(repo, dir, ce, fullname.buf, tag_removed);
     --					if (show_modified)
     --						show_ce(repo, dir, ce, fullname.buf, tag_modified);
     --			}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     --				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+		if ((dir->flags & DIR_SHOW_IGNORED) &&
     -+			!ce_excluded(dir, repo->index, fullname.buf, ce))
     -+			continue;
     -+		if (ce->ce_flags & CE_UPDATE)
     -+			continue;
     -+		if (show_cached || show_stage) {
     -+			if (!show_unmerged || ce_stage(ce))
     -+				show_ce(repo, dir, ce, fullname.buf,
     -+					ce_stage(ce) ? tag_unmerged :
     -+					(ce_skip_worktree(ce) ? tag_skip_worktree :
     -+						tag_cached));
     - 		}
     +-				if (errno != ENOENT && errno != ENOTDIR)
     +-				    error_errno("cannot lstat '%s'", fullname.buf);
     +-				if (show_deleted)
     +-					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     +-				if (show_modified)
     +-					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     +-			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
      +		if (ce_skip_worktree(ce))
      +			continue;
      +		err = lstat(fullname.buf, &st);
      +		if (err) {
     -+				if (show_deleted)
     -+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     -+				if (show_modified)
     -+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+		}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     ++			if (errno != ENOENT && errno != ENOTDIR)
     ++				error_errno("cannot lstat '%s'", fullname.buf);
     ++			if (show_deleted)
     ++				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++			if (show_modified)
     + 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     +-		}
     ++		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
      +			show_ce(repo, dir, ce, fullname.buf, tag_modified);
       	}
       
 3:  0c7830d07db ! 3:  e9c53186706 ls-files: add --deduplicate option
     @@ Metadata
      Author: ZheNing Hu [off-list ref]
      
       ## Commit message ##
     -    ls-files: add --deduplicate option
     +    ls-files.c: add --deduplicate option
      
          In order to provide users a better experience
          when viewing information about files in the index
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
       		if (ce->ce_flags & CE_UPDATE)
       			continue;
       		if (show_cached || show_stage) {
     -+			if (show_cached && skipping_duplicates && last_shown_ce &&
     ++			if (skipping_duplicates && last_shown_ce &&
      +				!strcmp(last_shown_ce->name,ce->name))
      +					continue;
       			if (!show_unmerged || ce_stage(ce))
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
       					ce_stage(ce) ? tag_unmerged :
       					(ce_skip_worktree(ce) ? tag_skip_worktree :
       						tag_cached));
     -+			if(show_cached && skipping_duplicates)
     ++			if (show_cached && skipping_duplicates)
      +				last_shown_ce = ce;
       		}
       		if (ce_skip_worktree(ce))
       			continue;
     -+		if (skipping_duplicates && last_shown_ce && !strcmp(last_shown_ce->name,ce->name))
     -+			continue;
     ++		if (skipping_duplicates && last_shown_ce &&
     ++			!strcmp(last_shown_ce->name,ce->name))
     ++				continue;
       		err = lstat(fullname.buf, &st);
       		if (err) {
     +-			if (errno != ENOENT && errno != ENOTDIR)
     +-				error_errno("cannot lstat '%s'", fullname.buf);
     +-			if (show_deleted)
      +			if (skipping_duplicates && show_deleted && show_modified)
     -+				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     + 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     +-			if (show_modified)
     +-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
      +			else {
     - 				if (show_deleted)
     - 					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     - 				if (show_modified)
     - 					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     --		}else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     ++				if (errno != ENOENT && errno != ENOTDIR)
     ++					error_errno("cannot lstat '%s'", fullname.buf);
     ++				if (show_deleted)
     ++					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++				if (show_modified)
     ++					show_ce(repo, dir, ce, fullname.buf, tag_modified);
      +			}
     -+		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     + 		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
       			show_ce(repo, dir, ce, fullname.buf, tag_modified);
      +		last_shown_ce = ce;
       	}
     @@ builtin/ls-files.c: int cmd_ls_files(int argc, const char **argv, const char *cm
       	};
       
      @@ builtin/ls-files.c: int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
     - 		tag_skip_worktree = "S ";
     - 		tag_resolve_undo = "U ";
     - 	}
     -+	if (show_tag && skipping_duplicates)
     + 		 * you also show the stage information.
     + 		 */
     + 		show_stage = 1;
     ++	if (show_tag || show_stage)
      +		skipping_duplicates = 0;
     - 	if (show_modified || show_others || show_deleted || (dir.flags & DIR_SHOW_IGNORED) || show_killed)
     - 		require_work_tree = 1;
     - 	if (show_unmerged)
     + 	if (dir.exclude_per_dir)
     + 		exc_given = 1;
     + 
      
       ## t/t3012-ls-files-dedup.sh (new) ##
      @@
     @@ t/t3012-ls-files-dedup.sh (new)
      +	>b.txt &&
      +	>delete.txt &&
      +	git add a.txt b.txt delete.txt &&
     -+	git commit -m master:1 &&
     ++	git commit -m base &&
      +	echo a >a.txt &&
      +	echo b >b.txt &&
      +	echo delete >delete.txt &&
      +	git add a.txt b.txt delete.txt &&
     -+	git commit -m master:2 &&
     -+	git checkout HEAD~ &&
     -+	git switch -c dev &&
     -+	test_when_finished "git switch master" &&
     ++	git commit -m tip &&
     ++	git tag tip &&
     ++	git reset --hard HEAD^ &&
      +	echo change >a.txt &&
     -+	git add a.txt &&
     -+	git commit -m dev:1 &&
     -+	test_must_fail git merge master &&
     ++	git commit -a -m side &&
     ++	git tag side
     ++'
     ++
     ++test_expect_success 'git ls-files --deduplicate to show unique unmerged path' '
     ++	test_must_fail git merge tip &&
      +	git ls-files --deduplicate >actual &&
      +	cat >expect <<-\EOF &&
      +	a.txt
     @@ t/t3012-ls-files-dedup.sh (new)
      +	delete.txt
      +	EOF
      +	test_cmp expect actual &&
     ++	git merge --abort
     ++'
     ++
     ++test_expect_success 'git ls-files -d -m --deduplicate with different display options' '
     ++	git reset --hard side &&
     ++	test_must_fail git merge tip &&
      +	rm delete.txt &&
      +	git ls-files -d -m --deduplicate >actual &&
      +	cat >expect <<-\EOF &&
     @@ t/t3012-ls-files-dedup.sh (new)
      +	delete.txt
      +	EOF
      +	test_cmp expect actual &&
     -+	git ls-files -d -m -t  --deduplicate >actual &&
     ++	git ls-files -d -m -t --deduplicate >actual &&
      +	cat >expect <<-\EOF &&
      +	C a.txt
      +	C a.txt
     @@ t/t3012-ls-files-dedup.sh (new)
      +	C delete.txt
      +	EOF
      +	test_cmp expect actual &&
     -+	git ls-files -d -m -c  --deduplicate >actual &&
     ++	git ls-files -d -m -c --deduplicate >actual &&
      +	cat >expect <<-\EOF &&
      +	a.txt
      +	b.txt
     @@ t/t3012-ls-files-dedup.sh (new)
      +	test_cmp expect actual &&
      +	git merge --abort
      +'
     ++
      +test_done

-- 
gitgitgadget

[PATCH v5 3/3] ls-files.c: add --deduplicate option

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-19 06:32:17

From: ZheNing Hu <redacted>

In order to provide users a better experience
when viewing information about files in the index
and the working tree, the `--deduplicate` option will suppress
some duplicate name under some conditions.

In a merge conflict, one file name of "git ls-files" output may
appear multiple times. For example,now there is an unmerged path
`a.c`,`a.c` will appear three times in the output of
"git ls-files".We can use "git ls-files --deduplicate" to output
`a.c` only one time.(unless `--stage` or `--unmerged` is
used to view all the detailed information in the index)

In addition, if you use both `--delete` and `--modify` at
the same time, The `--deduplicate` option
can also suppress file name output.

Additional instructions:
In order to display entries information,`deduplicate` suppresses
the output of duplicate file names, not the output of duplicate
entries information, so under the option of `-t`, `--stage`, `--unmerge`,
`--deduplicate` will have no effect.

Signed-off-by: ZheNing Hu <redacted>
---
 Documentation/git-ls-files.txt |  5 +++
 builtin/ls-files.c             | 32 ++++++++++++++---
 t/t3012-ls-files-dedup.sh      | 66 ++++++++++++++++++++++++++++++++++
 3 files changed, 98 insertions(+), 5 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index cbcf5263dd0..d11c8ade402 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--deduplicate]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -81,6 +82,10 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--deduplicate::
+	Suppress duplicate entries when there are unmerged paths in index
+	or `--deleted` and `--modified` are combined.
+
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 1454ab1ae6f..709d727c574 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int skipping_duplicates;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -301,6 +302,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 {
 	int i;
 	struct strbuf fullname = STRBUF_INIT;
+	const struct cache_entry *last_shown_ce;
 
 	/* For cached/deleted files we don't need to even do the readdir */
 	if (show_others || show_killed) {
@@ -314,6 +316,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 	}
 	if (! (show_cached || show_stage || show_deleted || show_modified))
 		return;
+	last_shown_ce = NULL;
 	for (i = 0; i < repo->index->cache_nr; i++) {
 		const struct cache_entry *ce = repo->index->cache[i];
 		struct stat st;
@@ -321,30 +324,46 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 
 		construct_fullname(&fullname, repo, ce);
 
+		if (skipping_duplicates && last_shown_ce &&
+			!strcmp(last_shown_ce->name,ce->name))
+				continue;
 		if ((dir->flags & DIR_SHOW_IGNORED) &&
 			!ce_excluded(dir, repo->index, fullname.buf, ce))
 			continue;
 		if (ce->ce_flags & CE_UPDATE)
 			continue;
 		if (show_cached || show_stage) {
+			if (skipping_duplicates && last_shown_ce &&
+				!strcmp(last_shown_ce->name,ce->name))
+					continue;
 			if (!show_unmerged || ce_stage(ce))
 				show_ce(repo, dir, ce, fullname.buf,
 					ce_stage(ce) ? tag_unmerged :
 					(ce_skip_worktree(ce) ? tag_skip_worktree :
 						tag_cached));
+			if (show_cached && skipping_duplicates)
+				last_shown_ce = ce;
 		}
 		if (ce_skip_worktree(ce))
 			continue;
+		if (skipping_duplicates && last_shown_ce &&
+			!strcmp(last_shown_ce->name,ce->name))
+				continue;
 		err = lstat(fullname.buf, &st);
 		if (err) {
-			if (errno != ENOENT && errno != ENOTDIR)
-				error_errno("cannot lstat '%s'", fullname.buf);
-			if (show_deleted)
+			if (skipping_duplicates && show_deleted && show_modified)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified)
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			else {
+				if (errno != ENOENT && errno != ENOTDIR)
+					error_errno("cannot lstat '%s'", fullname.buf);
+				if (show_deleted)
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified)
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			}
 		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
 			show_ce(repo, dir, ce, fullname.buf, tag_modified);
+		last_shown_ce = ce;
 	}
 
 	strbuf_release(&fullname);
@@ -571,6 +590,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0,"deduplicate",&skipping_duplicates,N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
@@ -610,6 +630,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 		 * you also show the stage information.
 		 */
 		show_stage = 1;
+	if (show_tag || show_stage)
+		skipping_duplicates = 0;
 	if (dir.exclude_per_dir)
 		exc_given = 1;
 
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 00000000000..2682b1f43a6
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='git ls-files --deduplicate test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	>a.txt &&
+	>b.txt &&
+	>delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m base &&
+	echo a >a.txt &&
+	echo b >b.txt &&
+	echo delete >delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m tip &&
+	git tag tip &&
+	git reset --hard HEAD^ &&
+	echo change >a.txt &&
+	git commit -a -m side &&
+	git tag side
+'
+
+test_expect_success 'git ls-files --deduplicate to show unique unmerged path' '
+	test_must_fail git merge tip &&
+	git ls-files --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_expect_success 'git ls-files -d -m --deduplicate with different display options' '
+	git reset --hard side &&
+	test_must_fail git merge tip &&
+	rm delete.txt &&
+	git ls-files -d -m --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -t --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	C a.txt
+	C a.txt
+	C a.txt
+	R delete.txt
+	C delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -c --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_done
-- 
gitgitgadget

[PATCH v5 2/3] ls_files.c: consolidate two for loops into one

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-19 06:32:38

From: ZheNing Hu <redacted>

Refactor the two for loops into one,skip showing the ce if it
has the same name as the previously shown one, only when doing so
won't lose information.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 70 +++++++++++++++++++---------------------------
 1 file changed, 29 insertions(+), 41 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index f1617260064..1454ab1ae6f 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,51 +312,39 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (show_killed)
 			show_killed_files(repo->index, dir);
 	}
-	if (show_cached || show_stage) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
+	if (! (show_cached || show_stage || show_deleted || show_modified))
+		return;
+	for (i = 0; i < repo->index->cache_nr; i++) {
+		const struct cache_entry *ce = repo->index->cache[i];
+		struct stat st;
+		int err;
 
-			construct_fullname(&fullname, repo, ce);
+		construct_fullname(&fullname, repo, ce);
 
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (show_unmerged && !ce_stage(ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			show_ce(repo, dir, ce, fullname.buf,
-				ce_stage(ce) ? tag_unmerged :
-				(ce_skip_worktree(ce) ? tag_skip_worktree :
-				 tag_cached));
+		if ((dir->flags & DIR_SHOW_IGNORED) &&
+			!ce_excluded(dir, repo->index, fullname.buf, ce))
+			continue;
+		if (ce->ce_flags & CE_UPDATE)
+			continue;
+		if (show_cached || show_stage) {
+			if (!show_unmerged || ce_stage(ce))
+				show_ce(repo, dir, ce, fullname.buf,
+					ce_stage(ce) ? tag_unmerged :
+					(ce_skip_worktree(ce) ? tag_skip_worktree :
+						tag_cached));
 		}
-	}
-	if (show_deleted || show_modified) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-			struct stat st;
-			int err;
-
-			construct_fullname(&fullname, repo, ce);
-
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			if (ce_skip_worktree(ce))
-				continue;
-			err = lstat(fullname.buf, &st);
-			if (err) {
-				if (errno != ENOENT && errno != ENOTDIR)
-				    error_errno("cannot lstat '%s'", fullname.buf);
-				if (show_deleted)
-					show_ce(repo, dir, ce, fullname.buf, tag_removed);
-				if (show_modified)
-					show_ce(repo, dir, ce, fullname.buf, tag_modified);
-			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
+		if (ce_skip_worktree(ce))
+			continue;
+		err = lstat(fullname.buf, &st);
+		if (err) {
+			if (errno != ENOENT && errno != ENOTDIR)
+				error_errno("cannot lstat '%s'", fullname.buf);
+			if (show_deleted)
+				show_ce(repo, dir, ce, fullname.buf, tag_removed);
+			if (show_modified)
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
-		}
+		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			show_ce(repo, dir, ce, fullname.buf, tag_modified);
 	}
 
 	strbuf_release(&fullname);
-- 
gitgitgadget

Re: [PATCH v5 2/3] ls_files.c: consolidate two for loops into one

From: Junio C Hamano <hidden>
Date: 2021-01-20 20:37:23

"ZheNing Hu via GitGitGadget" [off-list ref] writes:
From: ZheNing Hu <redacted>

Refactor the two for loops into one,skip showing the ce if it
has the same name as the previously shown one, only when doing so
won't lose information.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 70 +++++++++++++++++++---------------------------
 1 file changed, 29 insertions(+), 41 deletions(-)
This one needs a bit more work, but I like the basic structure of
the rewritten loop.
quoted hunk
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index f1617260064..1454ab1ae6f 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,51 +312,39 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (show_killed)
 			show_killed_files(repo->index, dir);
 	}
-	if (show_cached || show_stage) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
+	if (! (show_cached || show_stage || show_deleted || show_modified))
+		return;
If none of these four are given, nothing will be given after this
point, so returning early is good.
+	for (i = 0; i < repo->index->cache_nr; i++) {
+		const struct cache_entry *ce = repo->index->cache[i];
+		struct stat st;
+		int err;
 
+		construct_fullname(&fullname, repo, ce);
 
+		if ((dir->flags & DIR_SHOW_IGNORED) &&
+			!ce_excluded(dir, repo->index, fullname.buf, ce))
+			continue;
+		if (ce->ce_flags & CE_UPDATE)
+			continue;
The above two are common between the original two codepaths, and
merging them is good.
+		if (show_cached || show_stage) {
+			if (!show_unmerged || ce_stage(ce))
+				show_ce(repo, dir, ce, fullname.buf,
+					ce_stage(ce) ? tag_unmerged :
+					(ce_skip_worktree(ce) ? tag_skip_worktree :
+						tag_cached));
 		}
We would want to reduce the indentation level of the show_ce() by
consolidating the nested if/if to

		if ((show_cached || show_stage) &&
                    (!show_unmerged || ce_stage(ce)))
			show_ce(...);


Everything below from this point should be skipped (especially, the
call to lstat()) unless show_modified and/or show_deleted was asked
by the caller, i.e.  we want to insert

		if (!(show_deleted || show_modified))
			continue;

here, before we call ce_skip_worktree(), I think.
+		if (ce_skip_worktree(ce))
+			continue;
+		err = lstat(fullname.buf, &st);
+		if (err) {
+			if (errno != ENOENT && errno != ENOTDIR)
+				error_errno("cannot lstat '%s'", fullname.buf);
+			if (show_deleted)
+				show_ce(repo, dir, ce, fullname.buf, tag_removed);
+			if (show_modified)
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
-		}
+		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			show_ce(repo, dir, ce, fullname.buf, tag_modified);
 	}
And this part would look somewhat different if we take my earlier
suggestion for [1/3].

Thanks.

Re: [PATCH v5 2/3] ls_files.c: consolidate two for loops into one

From: 胡哲宁 <hidden>
Date: 2021-01-21 11:06:27

Junio C Hamano [off-list ref] 于2021年1月21日周四 上午4:27写道:
"ZheNing Hu via GitGitGadget" [off-list ref] writes:
quoted
From: ZheNing Hu <redacted>

Refactor the two for loops into one,skip showing the ce if it
has the same name as the previously shown one, only when doing so
won't lose information.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 70 +++++++++++++++++++---------------------------
 1 file changed, 29 insertions(+), 41 deletions(-)
This one needs a bit more work, but I like the basic structure of
the rewritten loop.
quoted
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index f1617260064..1454ab1ae6f 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,51 +312,39 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
              if (show_killed)
                      show_killed_files(repo->index, dir);
      }
-     if (show_cached || show_stage) {
-             for (i = 0; i < repo->index->cache_nr; i++) {
-                     const struct cache_entry *ce = repo->index->cache[i];
+     if (! (show_cached || show_stage || show_deleted || show_modified))
+             return;
If none of these four are given, nothing will be given after this
point, so returning early is good.
I understand.
quoted
+     for (i = 0; i < repo->index->cache_nr; i++) {
+             const struct cache_entry *ce = repo->index->cache[i];
+             struct stat st;
+             int err;

+             construct_fullname(&fullname, repo, ce);

+             if ((dir->flags & DIR_SHOW_IGNORED) &&
+                     !ce_excluded(dir, repo->index, fullname.buf, ce))
+                     continue;
+             if (ce->ce_flags & CE_UPDATE)
+                     continue;
The above two are common between the original two codepaths, and
merging them is good.
quoted
+             if (show_cached || show_stage) {
+                     if (!show_unmerged || ce_stage(ce))
+                             show_ce(repo, dir, ce, fullname.buf,
+                                     ce_stage(ce) ? tag_unmerged :
+                                     (ce_skip_worktree(ce) ? tag_skip_worktree :
+                                             tag_cached));
              }
We would want to reduce the indentation level of the show_ce() by
consolidating the nested if/if to

                if ((show_cached || show_stage) &&
                    (!show_unmerged || ce_stage(ce)))
                        show_ce(...);
The reason for this may be I gave
"if(show_cached || show_stage)" in 3/3
Added some logic.
Everything below from this point should be skipped (especially, the
call to lstat()) unless show_modified and/or show_deleted was asked
by the caller, i.e.  we want to insert

                if (!(show_deleted || show_modified))
                        continue;
I agree.
here, before we call ce_skip_worktree(), I think.
quoted
+             if (ce_skip_worktree(ce))
+                     continue;
+             err = lstat(fullname.buf, &st);
+             if (err) {
+                     if (errno != ENOENT && errno != ENOTDIR)
+                             error_errno("cannot lstat '%s'", fullname.buf);
+                     if (show_deleted)
+                             show_ce(repo, dir, ce, fullname.buf, tag_removed);
+                     if (show_modified)
                              show_ce(repo, dir, ce, fullname.buf, tag_modified);
-             }
+             } else if (show_modified && ie_modified(repo->index, ce, &st, 0))
+                     show_ce(repo, dir, ce, fullname.buf, tag_modified);
      }
And this part would look somewhat different if we take my earlier
suggestion for [1/3].
Fine.
Thanks.

[PATCH v5 1/3] ls_files.c: bugfix for --deleted and --modified

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-19 06:32:53

From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..f1617260064 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -347,9 +347,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			if (ce_skip_worktree(ce))
 				continue;
 			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
-				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			if (err) {
+				if (errno != ENOENT && errno != ENOTDIR)
+				    error_errno("cannot lstat '%s'", fullname.buf);
+				if (show_deleted)
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified)
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
 		}
 	}
-- 
gitgitgadget

Re: [PATCH v5 1/3] ls_files.c: bugfix for --deleted and --modified

From: Junio C Hamano <hidden>
Date: 2021-01-20 20:37:35

"ZheNing Hu via GitGitGadget" [off-list ref] writes:
From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
---
Thanks.  A few comments:

 * The error_errno() line is not indented correctly; I'll fix it up
   while queuing, but it would conflict with 2/3 as you'll be moving
   that line around.

 * When we say "error", we do not even know if the thing got removed
   or modified at all, so it is somewhat strange to report it as
   such (the path may be intact and the only issue may be that we
   cannot read the containing directory).  It is equally strange not
   to say anything on the path, and between the two, there isn't
   clearly a more correct answer.  What you implemented here does
   not change the traditional behaviour of reporting it as
   deleted/modified to "alert" the user, which I think is good.

 * The logic for modified entry looks a bit duplicated.  I wonder if
   the one at the end of this message reads better.  Renaming err to
   stat_err is optional, but I think the name makes it clear why it
   is sensible that these two places use the variable as a sign that
   the path was deleted and/or modified.
 			err = lstat(fullname.buf, &st);
+			if (err) {
+				if (errno != ENOENT && errno != ENOTDIR)
+				    error_errno("cannot lstat '%s'", fullname.buf);
+				if (show_deleted)
+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
+				if (show_modified)
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);

			stat_err = lstat(...);
			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
				error_errno("cannot lstat '%s'", fullname.buf);

			if (show_deleted && stat_err)
				show_ce(..., tag_removed);
			if (show_modified &&
			    (stat_err || ie_modified(..., &st, 0)))
				show_ce(..., tag_modified);

Re: [PATCH v5 1/3] ls_files.c: bugfix for --deleted and --modified

From: 胡哲宁 <hidden>
Date: 2021-01-21 10:02:01

Junio C Hamano [off-list ref] 于2021年1月21日周四 上午4:26写道:
"ZheNing Hu via GitGitGadget" [off-list ref] writes:
quoted
From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
---
Thanks.  A few comments:

 * The error_errno() line is not indented correctly; I'll fix it up
   while queuing, but it would conflict with 2/3 as you'll be moving
   that line around.
I might not have noticed,very Sorry.
 * When we say "error", we do not even know if the thing got removed
   or modified at all, so it is somewhat strange to report it as
   such (the path may be intact and the only issue may be that we
   cannot read the containing directory).  It is equally strange not
   to say anything on the path, and between the two, there isn't
   clearly a more correct answer.  What you implemented here does
   not change the traditional behaviour of reporting it as
   deleted/modified to "alert" the user, which I think is good.
Haha,thanks!
 * The logic for modified entry looks a bit duplicated.  I wonder if
   the one at the end of this message reads better.  Renaming err to
   stat_err is optional, but I think the name makes it clear why it
   is sensible that these two places use the variable as a sign that
   the path was deleted and/or modified.
quoted
                      err = lstat(fullname.buf, &st);
+                     if (err) {
+                             if (errno != ENOENT && errno != ENOTDIR)
+                                 error_errno("cannot lstat '%s'", fullname.buf);
+                             if (show_deleted)
+                                     show_ce(repo, dir, ce, fullname.buf, tag_removed);
+                             if (show_modified)
+                                     show_ce(repo, dir, ce, fullname.buf, tag_modified);
+                     } else if (show_modified && ie_modified(repo->index, ce, &st, 0))
                              show_ce(repo, dir, ce, fullname.buf, tag_modified);

                        stat_err = lstat(...);
                        if (stat_err && (errno != ENOENT && errno != ENOTDIR))
                                error_errno("cannot lstat '%s'", fullname.buf);

                        if (show_deleted && stat_err)
                                show_ce(..., tag_removed);
                        if (show_modified &&
                            (stat_err || ie_modified(..., &st, 0)))
                                show_ce(..., tag_modified);
Yes,"stat_err" may better then "err".
And putting the conditions together will make the code more compact.

Thanks for your comment:)

[PATCH v6 0/3] builtin/ls-files.c:add git ls-file --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-23 10:21:17

I am reading the source code of git ls-files and learned that git ls-files
may have duplicate files name when there are unmerged path in a branch merge
or when different options are used at the same time. Users may fell confuse
when they see these duplicate file names.

As Junio C Hamano said ,it have odd behaviour.

Therefore, we can provide an additional option to git ls-files to delete
those repeated information.

This fixes https://github.com/gitgitgadget/git/issues/198

Thanks!

ZheNing Hu (3):
  ls_files.c: bugfix for --deleted and --modified
  ls_files.c: consolidate two for loops into one
  ls-files.c: add --deduplicate option

 Documentation/git-ls-files.txt |  5 ++
 builtin/ls-files.c             | 85 ++++++++++++++++++++--------------
 t/t3012-ls-files-dedup.sh      | 66 ++++++++++++++++++++++++++
 3 files changed, 121 insertions(+), 35 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh


base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v6
Pull-Request: https://github.com/gitgitgadget/git/pull/832

Range-diff vs v5:

 1:  ec9464f6094 ! 1:  fbc38ce9075 ls_files.c: bugfix for --deleted and --modified
     @@ Commit message
      
       ## builtin/ls-files.c ##
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     + 		for (i = 0; i < repo->index->cache_nr; i++) {
     + 			const struct cache_entry *ce = repo->index->cache[i];
     + 			struct stat st;
     +-			int err;
     ++			int stat_err;
     + 
     + 			construct_fullname(&fullname, repo, ce);
     + 
     +@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     + 				continue;
       			if (ce_skip_worktree(ce))
       				continue;
     - 			err = lstat(fullname.buf, &st);
     +-			err = lstat(fullname.buf, &st);
      -			if (show_deleted && err)
     --				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++			stat_err = lstat(fullname.buf, &st);
     ++			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
     ++				error_errno("cannot lstat '%s'", fullname.buf);
     ++			if (stat_err && show_deleted)
     + 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
      -			if (show_modified && ie_modified(repo->index, ce, &st, 0))
     -+			if (err) {
     -+				if (errno != ENOENT && errno != ENOTDIR)
     -+				    error_errno("cannot lstat '%s'", fullname.buf);
     -+				if (show_deleted)
     -+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     -+				if (show_modified)
     +-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++			if (show_modified &&
     ++				(stat_err || ie_modified(repo->index, ce, &st, 0)))
      +					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     - 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
       		}
       	}
     + 
 2:  802ff802be8 ! 2:  3997d390883 ls_files.c: consolidate two for loops into one
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -	if (show_cached || show_stage) {
      -		for (i = 0; i < repo->index->cache_nr; i++) {
      -			const struct cache_entry *ce = repo->index->cache[i];
     -+	if (! (show_cached || show_stage || show_deleted || show_modified))
     -+		return;
     -+	for (i = 0; i < repo->index->cache_nr; i++) {
     -+		const struct cache_entry *ce = repo->index->cache[i];
     -+		struct stat st;
     -+		int err;
     - 
     +-
      -			construct_fullname(&fullname, repo, ce);
     -+		construct_fullname(&fullname, repo, ce);
     - 
     +-
      -			if ((dir->flags & DIR_SHOW_IGNORED) &&
      -			    !ce_excluded(dir, repo->index, fullname.buf, ce))
      -				continue;
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -				ce_stage(ce) ? tag_unmerged :
      -				(ce_skip_worktree(ce) ? tag_skip_worktree :
      -				 tag_cached));
     -+		if ((dir->flags & DIR_SHOW_IGNORED) &&
     -+			!ce_excluded(dir, repo->index, fullname.buf, ce))
     -+			continue;
     -+		if (ce->ce_flags & CE_UPDATE)
     -+			continue;
     -+		if (show_cached || show_stage) {
     -+			if (!show_unmerged || ce_stage(ce))
     -+				show_ce(repo, dir, ce, fullname.buf,
     -+					ce_stage(ce) ? tag_unmerged :
     -+					(ce_skip_worktree(ce) ? tag_skip_worktree :
     -+						tag_cached));
     - 		}
     +-		}
      -	}
      -	if (show_deleted || show_modified) {
      -		for (i = 0; i < repo->index->cache_nr; i++) {
      -			const struct cache_entry *ce = repo->index->cache[i];
      -			struct stat st;
     --			int err;
     --
     +-			int stat_err;
     ++	if (! (show_cached || show_stage || show_deleted || show_modified))
     ++		return;
     ++	for (i = 0; i < repo->index->cache_nr; i++) {
     ++		const struct cache_entry *ce = repo->index->cache[i];
     ++		struct stat st;
     ++		int stat_err;
     + 
      -			construct_fullname(&fullname, repo, ce);
     --
     ++		construct_fullname(&fullname, repo, ce);
     + 
      -			if ((dir->flags & DIR_SHOW_IGNORED) &&
      -			    !ce_excluded(dir, repo->index, fullname.buf, ce))
      -				continue;
     @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_s
      -				continue;
      -			if (ce_skip_worktree(ce))
      -				continue;
     --			err = lstat(fullname.buf, &st);
     --			if (err) {
     --				if (errno != ENOENT && errno != ENOTDIR)
     --				    error_errno("cannot lstat '%s'", fullname.buf);
     --				if (show_deleted)
     --					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     --				if (show_modified)
     +-			stat_err = lstat(fullname.buf, &st);
     +-			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
     +-				error_errno("cannot lstat '%s'", fullname.buf);
     +-			if (stat_err && show_deleted)
     +-				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     +-			if (show_modified &&
     +-				(stat_err || ie_modified(repo->index, ce, &st, 0)))
      -					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     --			} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     ++		if ((dir->flags & DIR_SHOW_IGNORED) &&
     ++			!ce_excluded(dir, repo->index, fullname.buf, ce))
     ++			continue;
     ++		if (ce->ce_flags & CE_UPDATE)
     ++			continue;
     ++		if (show_cached || show_stage) {
     ++			if (!show_unmerged || ce_stage(ce))
     ++				show_ce(repo, dir, ce, fullname.buf,
     ++					ce_stage(ce) ? tag_unmerged :
     ++					(ce_skip_worktree(ce) ? tag_skip_worktree :
     ++						tag_cached));
     + 		}
     ++		if (!show_deleted && !show_modified)
     ++			continue;
      +		if (ce_skip_worktree(ce))
      +			continue;
     -+		err = lstat(fullname.buf, &st);
     -+		if (err) {
     -+			if (errno != ENOENT && errno != ENOTDIR)
     -+				error_errno("cannot lstat '%s'", fullname.buf);
     -+			if (show_deleted)
     -+				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     -+			if (show_modified)
     - 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     --		}
     -+		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     -+			show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++		stat_err = lstat(fullname.buf, &st);
     ++		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
     ++			error_errno("cannot lstat '%s'", fullname.buf);
     ++		if (stat_err && show_deleted)
     ++			show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++		if (show_modified &&
     ++			(stat_err || ie_modified(repo->index, ce, &st, 0)))
     ++				show_ce(repo, dir, ce, fullname.buf, tag_modified);
       	}
       
       	strbuf_release(&fullname);
 3:  e9c53186706 ! 3:  07b603fd97c ls-files.c: add --deduplicate option
     @@ builtin/ls-files.c: static int line_terminator = '\n';
       static const char *prefix;
       static int max_prefix_len;
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - {
     - 	int i;
     - 	struct strbuf fullname = STRBUF_INIT;
     -+	const struct cache_entry *last_shown_ce;
     - 
     - 	/* For cached/deleted files we don't need to even do the readdir */
     - 	if (show_others || show_killed) {
     -@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 	}
     - 	if (! (show_cached || show_stage || show_deleted || show_modified))
     - 		return;
     -+	last_shown_ce = NULL;
     - 	for (i = 0; i < repo->index->cache_nr; i++) {
     - 		const struct cache_entry *ce = repo->index->cache[i];
     - 		struct stat st;
     -@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     - 
     - 		construct_fullname(&fullname, repo, ce);
     - 
     -+		if (skipping_duplicates && last_shown_ce &&
     -+			!strcmp(last_shown_ce->name,ce->name))
     -+				continue;
     - 		if ((dir->flags & DIR_SHOW_IGNORED) &&
     - 			!ce_excluded(dir, repo->index, fullname.buf, ce))
       			continue;
       		if (ce->ce_flags & CE_UPDATE)
       			continue;
     - 		if (show_cached || show_stage) {
     -+			if (skipping_duplicates && last_shown_ce &&
     -+				!strcmp(last_shown_ce->name,ce->name))
     -+					continue;
     - 			if (!show_unmerged || ce_stage(ce))
     +-		if (show_cached || show_stage) {
     +-			if (!show_unmerged || ce_stage(ce))
     ++		if ((show_cached || show_stage) &&
     ++			(!show_unmerged || ce_stage(ce))) {
       				show_ce(repo, dir, ce, fullname.buf,
       					ce_stage(ce) ? tag_unmerged :
       					(ce_skip_worktree(ce) ? tag_skip_worktree :
       						tag_cached));
     -+			if (show_cached && skipping_duplicates)
     -+				last_shown_ce = ce;
     ++			if (skipping_duplicates)
     ++				goto skip_to_next_name;
       		}
     - 		if (ce_skip_worktree(ce))
     + 		if (!show_deleted && !show_modified)
       			continue;
     -+		if (skipping_duplicates && last_shown_ce &&
     -+			!strcmp(last_shown_ce->name,ce->name))
     -+				continue;
     - 		err = lstat(fullname.buf, &st);
     - 		if (err) {
     --			if (errno != ENOENT && errno != ENOTDIR)
     --				error_errno("cannot lstat '%s'", fullname.buf);
     --			if (show_deleted)
     -+			if (skipping_duplicates && show_deleted && show_modified)
     - 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
     --			if (show_modified)
     --				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+			else {
     -+				if (errno != ENOENT && errno != ENOTDIR)
     -+					error_errno("cannot lstat '%s'", fullname.buf);
     -+				if (show_deleted)
     -+					show_ce(repo, dir, ce, fullname.buf, tag_removed);
     -+				if (show_modified)
     -+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+			}
     - 		} else if (show_modified && ie_modified(repo->index, ce, &st, 0))
     - 			show_ce(repo, dir, ce, fullname.buf, tag_modified);
     -+		last_shown_ce = ce;
     +@@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
     + 		stat_err = lstat(fullname.buf, &st);
     + 		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
     + 			error_errno("cannot lstat '%s'", fullname.buf);
     +-		if (stat_err && show_deleted)
     ++		if (stat_err && show_deleted) {
     + 			show_ce(repo, dir, ce, fullname.buf, tag_removed);
     ++			if (skipping_duplicates)
     ++				goto skip_to_next_name;
     ++		}
     + 		if (show_modified &&
     +-			(stat_err || ie_modified(repo->index, ce, &st, 0)))
     ++			(stat_err || ie_modified(repo->index, ce, &st, 0))) {
     + 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
     ++			if (skipping_duplicates)
     ++				goto skip_to_next_name;
     ++		}
     ++		continue;
     ++skip_to_next_name:
     ++		{
     ++			int j;
     ++			struct cache_entry **cache = repo->index->cache;
     ++			for (j = i + 1; j < repo->index->cache_nr; j++)
     ++				if (strcmp(ce->name, cache[j]->name))
     ++					break;
     ++			i = j - 1; /* compensate for outer for loop */
     ++		}
       	}
       
       	strbuf_release(&fullname);

-- 
gitgitgadget

[PATCH v6 1/3] ls_files.c: bugfix for --deleted and --modified

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-23 10:21:17

From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..1e264bd1329 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -335,7 +335,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
 			struct stat st;
-			int err;
+			int stat_err;
 
 			construct_fullname(&fullname, repo, ce);
 
@@ -346,11 +346,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 				continue;
 			if (ce_skip_worktree(ce))
 				continue;
-			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
+			stat_err = lstat(fullname.buf, &st);
+			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
+				error_errno("cannot lstat '%s'", fullname.buf);
+			if (stat_err && show_deleted)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			if (show_modified &&
+				(stat_err || ie_modified(repo->index, ce, &st, 0)))
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
 		}
 	}
 
-- 
gitgitgadget

[PATCH v6 2/3] ls_files.c: consolidate two for loops into one

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-23 10:21:17

From: ZheNing Hu <redacted>

Refactor the two for loops into one,skip showing the ce if it
has the same name as the previously shown one, only when doing so
won't lose information.

Signed-off-by: ZheNing Hu <redacted>
---
 builtin/ls-files.c | 70 ++++++++++++++++++++--------------------------
 1 file changed, 30 insertions(+), 40 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 1e264bd1329..966c0ab0296 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,49 +312,39 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (show_killed)
 			show_killed_files(repo->index, dir);
 	}
-	if (show_cached || show_stage) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-
-			construct_fullname(&fullname, repo, ce);
-
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (show_unmerged && !ce_stage(ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			show_ce(repo, dir, ce, fullname.buf,
-				ce_stage(ce) ? tag_unmerged :
-				(ce_skip_worktree(ce) ? tag_skip_worktree :
-				 tag_cached));
-		}
-	}
-	if (show_deleted || show_modified) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-			struct stat st;
-			int stat_err;
+	if (! (show_cached || show_stage || show_deleted || show_modified))
+		return;
+	for (i = 0; i < repo->index->cache_nr; i++) {
+		const struct cache_entry *ce = repo->index->cache[i];
+		struct stat st;
+		int stat_err;
 
-			construct_fullname(&fullname, repo, ce);
+		construct_fullname(&fullname, repo, ce);
 
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			if (ce_skip_worktree(ce))
-				continue;
-			stat_err = lstat(fullname.buf, &st);
-			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
-				error_errno("cannot lstat '%s'", fullname.buf);
-			if (stat_err && show_deleted)
-				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified &&
-				(stat_err || ie_modified(repo->index, ce, &st, 0)))
-					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+		if ((dir->flags & DIR_SHOW_IGNORED) &&
+			!ce_excluded(dir, repo->index, fullname.buf, ce))
+			continue;
+		if (ce->ce_flags & CE_UPDATE)
+			continue;
+		if (show_cached || show_stage) {
+			if (!show_unmerged || ce_stage(ce))
+				show_ce(repo, dir, ce, fullname.buf,
+					ce_stage(ce) ? tag_unmerged :
+					(ce_skip_worktree(ce) ? tag_skip_worktree :
+						tag_cached));
 		}
+		if (!show_deleted && !show_modified)
+			continue;
+		if (ce_skip_worktree(ce))
+			continue;
+		stat_err = lstat(fullname.buf, &st);
+		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
+			error_errno("cannot lstat '%s'", fullname.buf);
+		if (stat_err && show_deleted)
+			show_ce(repo, dir, ce, fullname.buf, tag_removed);
+		if (show_modified &&
+			(stat_err || ie_modified(repo->index, ce, &st, 0)))
+				show_ce(repo, dir, ce, fullname.buf, tag_modified);
 	}
 
 	strbuf_release(&fullname);
-- 
gitgitgadget

[PATCH v6 3/3] ls-files.c: add --deduplicate option

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-23 10:21:17

From: ZheNing Hu <redacted>

In order to provide users a better experience
when viewing information about files in the index
and the working tree, the `--deduplicate` option will suppress
some duplicate name under some conditions.

In a merge conflict, one file name of "git ls-files" output may
appear multiple times. For example,now there is an unmerged path
`a.c`,`a.c` will appear three times in the output of
"git ls-files".We can use "git ls-files --deduplicate" to output
`a.c` only one time.(unless `--stage` or `--unmerged` is
used to view all the detailed information in the index)

In addition, if you use both `--delete` and `--modify` at
the same time, The `--deduplicate` option
can also suppress file name output.

Additional instructions:
In order to display entries information,`deduplicate` suppresses
the output of duplicate file names, not the output of duplicate
entries information, so under the option of `-t`, `--stage`, `--unmerge`,
`--deduplicate` will have no effect.

Signed-off-by: ZheNing Hu <redacted>
---
 Documentation/git-ls-files.txt |  5 +++
 builtin/ls-files.c             | 30 +++++++++++++---
 t/t3012-ls-files-dedup.sh      | 66 ++++++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+), 4 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index cbcf5263dd0..d11c8ade402 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--deduplicate]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -81,6 +82,10 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--deduplicate::
+	Suppress duplicate entries when there are unmerged paths in index
+	or `--deleted` and `--modified` are combined.
+
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 966c0ab0296..fb9cf50d764 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int skipping_duplicates;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -326,12 +327,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			continue;
 		if (ce->ce_flags & CE_UPDATE)
 			continue;
-		if (show_cached || show_stage) {
-			if (!show_unmerged || ce_stage(ce))
+		if ((show_cached || show_stage) &&
+			(!show_unmerged || ce_stage(ce))) {
 				show_ce(repo, dir, ce, fullname.buf,
 					ce_stage(ce) ? tag_unmerged :
 					(ce_skip_worktree(ce) ? tag_skip_worktree :
 						tag_cached));
+			if (skipping_duplicates)
+				goto skip_to_next_name;
 		}
 		if (!show_deleted && !show_modified)
 			continue;
@@ -340,11 +343,27 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		stat_err = lstat(fullname.buf, &st);
 		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
 			error_errno("cannot lstat '%s'", fullname.buf);
-		if (stat_err && show_deleted)
+		if (stat_err && show_deleted) {
 			show_ce(repo, dir, ce, fullname.buf, tag_removed);
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
 		if (show_modified &&
-			(stat_err || ie_modified(repo->index, ce, &st, 0)))
+			(stat_err || ie_modified(repo->index, ce, &st, 0))) {
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
+		continue;
+skip_to_next_name:
+		{
+			int j;
+			struct cache_entry **cache = repo->index->cache;
+			for (j = i + 1; j < repo->index->cache_nr; j++)
+				if (strcmp(ce->name, cache[j]->name))
+					break;
+			i = j - 1; /* compensate for outer for loop */
+		}
 	}
 
 	strbuf_release(&fullname);
@@ -571,6 +590,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0,"deduplicate",&skipping_duplicates,N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
@@ -610,6 +630,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 		 * you also show the stage information.
 		 */
 		show_stage = 1;
+	if (show_tag || show_stage)
+		skipping_duplicates = 0;
 	if (dir.exclude_per_dir)
 		exc_given = 1;
 
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 00000000000..2682b1f43a6
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='git ls-files --deduplicate test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	>a.txt &&
+	>b.txt &&
+	>delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m base &&
+	echo a >a.txt &&
+	echo b >b.txt &&
+	echo delete >delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m tip &&
+	git tag tip &&
+	git reset --hard HEAD^ &&
+	echo change >a.txt &&
+	git commit -a -m side &&
+	git tag side
+'
+
+test_expect_success 'git ls-files --deduplicate to show unique unmerged path' '
+	test_must_fail git merge tip &&
+	git ls-files --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_expect_success 'git ls-files -d -m --deduplicate with different display options' '
+	git reset --hard side &&
+	test_must_fail git merge tip &&
+	rm delete.txt &&
+	git ls-files -d -m --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -t --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	C a.txt
+	C a.txt
+	C a.txt
+	R delete.txt
+	C delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -c --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_done
-- 
gitgitgadget

[PATCH v7 1/3] ls_files.c: bugfix for --deleted and --modified

From: Junio C Hamano <hidden>
Date: 2021-01-23 19:54:36

From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
[jc: fixed misindented code]
Signed-off-by: Junio C Hamano <redacted>
---
 builtin/ls-files.c | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b8..ce6f6ad00e 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -335,7 +335,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
 			struct stat st;
-			int err;
+			int stat_err;
 
 			construct_fullname(&fullname, repo, ce);
 
@@ -346,10 +346,13 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 				continue;
 			if (ce_skip_worktree(ce))
 				continue;
-			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
+			stat_err = lstat(fullname.buf, &st);
+			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
+				error_errno("cannot lstat '%s'", fullname.buf);
+			if (stat_err && show_deleted)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
+			if (show_modified &&
+			    (stat_err || ie_modified(repo->index, ce, &st, 0)))
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
 		}
 	}
-- 
2.30.0-491-g302c625a7b

[PATCH v7 2/3] ls_files.c: consolidate two for loops into one

From: Junio C Hamano <hidden>
Date: 2021-01-23 19:54:36

From: ZheNing Hu <redacted>

This will make it easier to show only one entry per filename in the
next step.

Signed-off-by: ZheNing Hu <redacted>
[jc: corrected the log message]
Signed-off-by: Junio C Hamano <redacted>
---
 builtin/ls-files.c | 63 ++++++++++++++++++++--------------------------
 1 file changed, 27 insertions(+), 36 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index ce6f6ad00e..e94d724aff 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,49 +312,40 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (show_killed)
 			show_killed_files(repo->index, dir);
 	}
-	if (show_cached || show_stage) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
 
-			construct_fullname(&fullname, repo, ce);
+	if (!(show_cached || show_stage || show_deleted || show_modified))
+		return;
+	for (i = 0; i < repo->index->cache_nr; i++) {
+		const struct cache_entry *ce = repo->index->cache[i];
+		struct stat st;
+		int stat_err;
 
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (show_unmerged && !ce_stage(ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
+		construct_fullname(&fullname, repo, ce);
+
+		if ((dir->flags & DIR_SHOW_IGNORED) &&
+			!ce_excluded(dir, repo->index, fullname.buf, ce))
+			continue;
+		if (ce->ce_flags & CE_UPDATE)
+			continue;
+		if ((show_cached || show_stage) &&
+		    (!show_unmerged || ce_stage(ce)))
 			show_ce(repo, dir, ce, fullname.buf,
 				ce_stage(ce) ? tag_unmerged :
 				(ce_skip_worktree(ce) ? tag_skip_worktree :
 				 tag_cached));
-		}
-	}
-	if (show_deleted || show_modified) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-			struct stat st;
-			int stat_err;
-
-			construct_fullname(&fullname, repo, ce);
 
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			if (ce_skip_worktree(ce))
-				continue;
-			stat_err = lstat(fullname.buf, &st);
-			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
-				error_errno("cannot lstat '%s'", fullname.buf);
-			if (stat_err && show_deleted)
-				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified &&
-			    (stat_err || ie_modified(repo->index, ce, &st, 0)))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
-		}
+		if (!(show_deleted || show_modified))
+			continue;
+		if (ce_skip_worktree(ce))
+			continue;
+		stat_err = lstat(fullname.buf, &st);
+		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
+			error_errno("cannot lstat '%s'", fullname.buf);
+		if (stat_err && show_deleted)
+			show_ce(repo, dir, ce, fullname.buf, tag_removed);
+		if (show_modified &&
+		    (stat_err || ie_modified(repo->index, ce, &st, 0)))
+			show_ce(repo, dir, ce, fullname.buf, tag_modified);
 	}
 
 	strbuf_release(&fullname);
-- 
2.30.0-491-g302c625a7b

[PATCH v7 3/3] ls-files.c: add --deduplicate option

From: Junio C Hamano <hidden>
Date: 2021-01-23 19:54:36

From: ZheNing Hu <redacted>

During a merge conflict, the name of a file may appear multiple
times in "git ls-files" output, once for each stage.  If you use
both `--delete` and `--modify` at the same time, the output may
mention a deleted file twice.

When none of the '-t', '-u', or '-s' options is in use, these
duplicate entries do not add much value to the output.

Introduce a new '--deduplicate' option to suppress them.

Signed-off-by: ZheNing Hu <redacted>
[jc: extended doc and rewritten commit log]
Signed-off-by: Junio C Hamano <redacted>
---
 Documentation/git-ls-files.txt |  8 +++++
 builtin/ls-files.c             | 31 ++++++++++++++--
 t/t3012-ls-files-dedup.sh      | 66 ++++++++++++++++++++++++++++++++++
 3 files changed, 102 insertions(+), 3 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index 0a3b5265b3..6d11ab506b 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--deduplicate]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -80,6 +81,13 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--deduplicate::
+	When only filenames are shown, suppress duplicates that may
+	come from having multiple stages during a merge, or giving
+	`--deleted` and `--modified` option at the same time.
+	When any of the `-t`, `--unmerged`, or `--stage` option is
+	in use, this option has no effect.
+
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index e94d724aff..f6f9e483b2 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int skipping_duplicates;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -328,11 +329,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (ce->ce_flags & CE_UPDATE)
 			continue;
 		if ((show_cached || show_stage) &&
-		    (!show_unmerged || ce_stage(ce)))
+		    (!show_unmerged || ce_stage(ce))) {
 			show_ce(repo, dir, ce, fullname.buf,
 				ce_stage(ce) ? tag_unmerged :
 				(ce_skip_worktree(ce) ? tag_skip_worktree :
 				 tag_cached));
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
 
 		if (!(show_deleted || show_modified))
 			continue;
@@ -341,11 +345,28 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		stat_err = lstat(fullname.buf, &st);
 		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
 			error_errno("cannot lstat '%s'", fullname.buf);
-		if (stat_err && show_deleted)
+		if (stat_err && show_deleted) {
 			show_ce(repo, dir, ce, fullname.buf, tag_removed);
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
 		if (show_modified &&
-		    (stat_err || ie_modified(repo->index, ce, &st, 0)))
+		    (stat_err || ie_modified(repo->index, ce, &st, 0))) {
 			show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
+		continue;
+
+skip_to_next_name:
+		{
+			int j;
+			struct cache_entry **cache = repo->index->cache;
+			for (j = i + 1; j < repo->index->cache_nr; j++)
+				if (strcmp(ce->name, cache[j]->name))
+					break;
+			i = j - 1; /* compensate for the for loop */
+		}
 	}
 
 	strbuf_release(&fullname);
@@ -572,6 +593,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0, "deduplicate", &skipping_duplicates,
+			 N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
@@ -611,6 +634,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 		 * you also show the stage information.
 		 */
 		show_stage = 1;
+	if (show_tag || show_stage)
+		skipping_duplicates = 0;
 	if (dir.exclude_per_dir)
 		exc_given = 1;
 
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 0000000000..2682b1f43a
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='git ls-files --deduplicate test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	>a.txt &&
+	>b.txt &&
+	>delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m base &&
+	echo a >a.txt &&
+	echo b >b.txt &&
+	echo delete >delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m tip &&
+	git tag tip &&
+	git reset --hard HEAD^ &&
+	echo change >a.txt &&
+	git commit -a -m side &&
+	git tag side
+'
+
+test_expect_success 'git ls-files --deduplicate to show unique unmerged path' '
+	test_must_fail git merge tip &&
+	git ls-files --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_expect_success 'git ls-files -d -m --deduplicate with different display options' '
+	git reset --hard side &&
+	test_must_fail git merge tip &&
+	rm delete.txt &&
+	git ls-files -d -m --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -t --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	C a.txt
+	C a.txt
+	C a.txt
+	R delete.txt
+	C delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -c --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_done
-- 
2.30.0-491-g302c625a7b

[PATCH v7 0/3] builtin/ls-files.c:add git ls-file --dedup option

From: 阿德烈 via GitGitGadget <hidden>
Date: 2021-01-24 10:55:36

I am reading the source code of git ls-files and learned that git ls-files
may have duplicate files name when there are unmerged path in a branch merge
or when different options are used at the same time. Users may fell confuse
when they see these duplicate file names.

As Junio C Hamano said ,it have odd behaviour.

Therefore, we can provide an additional option to git ls-files to delete
those repeated information.

This fixes https://github.com/gitgitgadget/git/issues/198

Thanks!

ZheNing Hu (3):
  ls_files.c: bugfix for --deleted and --modified
  ls_files.c: consolidate two for loops into one
  ls-files.c: add --deduplicate option

 Documentation/git-ls-files.txt |  8 ++++
 builtin/ls-files.c             | 85 ++++++++++++++++++++--------------
 t/t3012-ls-files-dedup.sh      | 66 ++++++++++++++++++++++++++
 3 files changed, 124 insertions(+), 35 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh


base-commit: 6d3ef5b467eccd2769f1aa1c555d317d3c8dc707
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-832%2Fadlternative%2Fls-files-dedup-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-832/adlternative/ls-files-dedup-v7
Pull-Request: https://github.com/gitgitgadget/git/pull/832

Range-diff vs v6:

 1:  fbc38ce9075 ! 1:  8b02367a359 ls_files.c: bugfix for --deleted and --modified
     @@ Commit message
          ie_modified() when lstat() has failed.
      
          Signed-off-by: ZheNing Hu [off-list ref]
     +    [jc: fixed misindented code]
     +    Signed-off-by: Junio C Hamano [off-list ref]
      
       ## builtin/ls-files.c ##
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
 2:  3997d390883 ! 2:  f9d5e44d2c0 ls_files.c: consolidate two for loops into one
     @@ Metadata
       ## Commit message ##
          ls_files.c: consolidate two for loops into one
      
     -    Refactor the two for loops into one,skip showing the ce if it
     -    has the same name as the previously shown one, only when doing so
     -    won't lose information.
     +    This will make it easier to show only one entry per filename in the
     +    next step.
      
          Signed-off-by: ZheNing Hu [off-list ref]
     +    [jc: corrected the log message]
     +    Signed-off-by: Junio C Hamano [off-list ref]
      
       ## builtin/ls-files.c ##
      @@ builtin/ls-files.c: static void show_files(struct repository *repo, struct dir_struct *dir)
 3:  07b603fd97c ! 3:  384f77a4c18 ls-files.c: add --deduplicate option
     @@ Metadata
       ## Commit message ##
          ls-files.c: add --deduplicate option
      
     -    In order to provide users a better experience
     -    when viewing information about files in the index
     -    and the working tree, the `--deduplicate` option will suppress
     -    some duplicate name under some conditions.
     +    During a merge conflict, the name of a file may appear multiple
     +    times in "git ls-files" output, once for each stage.  If you use
     +    both `--delete` and `--modify` at the same time, the output may
     +    mention a deleted file twice.
      
     -    In a merge conflict, one file name of "git ls-files" output may
     -    appear multiple times. For example,now there is an unmerged path
     -    `a.c`,`a.c` will appear three times in the output of
     -    "git ls-files".We can use "git ls-files --deduplicate" to output
     -    `a.c` only one time.(unless `--stage` or `--unmerged` is
     -    used to view all the detailed information in the index)
     +    When none of the '-t', '-u', or '-s' options is in use, these
     +    duplicate entries do not add much value to the output.
      
     -    In addition, if you use both `--delete` and `--modify` at
     -    the same time, The `--deduplicate` option
     -    can also suppress file name output.
     -
     -    Additional instructions:
     -    In order to display entries information,`deduplicate` suppresses
     -    the output of duplicate file names, not the output of duplicate
     -    entries information, so under the option of `-t`, `--stage`, `--unmerge`,
     -    `--deduplicate` will have no effect.
     +    Introduce a new '--deduplicate' option to suppress them.
      
          Signed-off-by: ZheNing Hu [off-list ref]
     +    [jc: extended doc and rewritten commit log]
     +    Signed-off-by: Junio C Hamano [off-list ref]
      
       ## Documentation/git-ls-files.txt ##
      @@ Documentation/git-ls-files.txt: SYNOPSIS
     @@ Documentation/git-ls-files.txt: OPTIONS
       	See OUTPUT below for more information.
       
      +--deduplicate::
     -+	Suppress duplicate entries when there are unmerged paths in index
     -+	or `--deleted` and `--modified` are combined.
     ++	When only filenames are shown, suppress duplicates that may
     ++	come from having multiple stages during a merge, or giving
     ++	`--deleted` and `--modified` option at the same time.
     ++	When any of the `-t`, `--unmerged`, or `--stage` option is
     ++	in use, this option has no effect.
      +
       -x <pattern>::
       --exclude=<pattern>::

-- 
gitgitgadget

[PATCH v7 1/3] ls_files.c: bugfix for --deleted and --modified

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-24 10:55:36

From: ZheNing Hu <redacted>

This situation may occur in the original code: lstat() failed
but we use `&st` to feed ie_modified() later.

Therefore, we can directly execute show_ce without the judgment of
ie_modified() when lstat() has failed.

Signed-off-by: ZheNing Hu <redacted>
[jc: fixed misindented code]
Signed-off-by: Junio C Hamano <redacted>
---
 builtin/ls-files.c | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index c8eae899b82..1e264bd1329 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -335,7 +335,7 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		for (i = 0; i < repo->index->cache_nr; i++) {
 			const struct cache_entry *ce = repo->index->cache[i];
 			struct stat st;
-			int err;
+			int stat_err;
 
 			construct_fullname(&fullname, repo, ce);
 
@@ -346,11 +346,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 				continue;
 			if (ce_skip_worktree(ce))
 				continue;
-			err = lstat(fullname.buf, &st);
-			if (show_deleted && err)
+			stat_err = lstat(fullname.buf, &st);
+			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
+				error_errno("cannot lstat '%s'", fullname.buf);
+			if (stat_err && show_deleted)
 				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified && ie_modified(repo->index, ce, &st, 0))
-				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			if (show_modified &&
+				(stat_err || ie_modified(repo->index, ce, &st, 0)))
+					show_ce(repo, dir, ce, fullname.buf, tag_modified);
 		}
 	}
 
-- 
gitgitgadget

[PATCH v7 2/3] ls_files.c: consolidate two for loops into one

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-24 10:55:36

From: ZheNing Hu <redacted>

This will make it easier to show only one entry per filename in the
next step.

Signed-off-by: ZheNing Hu <redacted>
[jc: corrected the log message]
Signed-off-by: Junio C Hamano <redacted>
---
 builtin/ls-files.c | 70 ++++++++++++++++++++--------------------------
 1 file changed, 30 insertions(+), 40 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 1e264bd1329..966c0ab0296 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -312,49 +312,39 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		if (show_killed)
 			show_killed_files(repo->index, dir);
 	}
-	if (show_cached || show_stage) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-
-			construct_fullname(&fullname, repo, ce);
-
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (show_unmerged && !ce_stage(ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			show_ce(repo, dir, ce, fullname.buf,
-				ce_stage(ce) ? tag_unmerged :
-				(ce_skip_worktree(ce) ? tag_skip_worktree :
-				 tag_cached));
-		}
-	}
-	if (show_deleted || show_modified) {
-		for (i = 0; i < repo->index->cache_nr; i++) {
-			const struct cache_entry *ce = repo->index->cache[i];
-			struct stat st;
-			int stat_err;
+	if (! (show_cached || show_stage || show_deleted || show_modified))
+		return;
+	for (i = 0; i < repo->index->cache_nr; i++) {
+		const struct cache_entry *ce = repo->index->cache[i];
+		struct stat st;
+		int stat_err;
 
-			construct_fullname(&fullname, repo, ce);
+		construct_fullname(&fullname, repo, ce);
 
-			if ((dir->flags & DIR_SHOW_IGNORED) &&
-			    !ce_excluded(dir, repo->index, fullname.buf, ce))
-				continue;
-			if (ce->ce_flags & CE_UPDATE)
-				continue;
-			if (ce_skip_worktree(ce))
-				continue;
-			stat_err = lstat(fullname.buf, &st);
-			if (stat_err && (errno != ENOENT && errno != ENOTDIR))
-				error_errno("cannot lstat '%s'", fullname.buf);
-			if (stat_err && show_deleted)
-				show_ce(repo, dir, ce, fullname.buf, tag_removed);
-			if (show_modified &&
-				(stat_err || ie_modified(repo->index, ce, &st, 0)))
-					show_ce(repo, dir, ce, fullname.buf, tag_modified);
+		if ((dir->flags & DIR_SHOW_IGNORED) &&
+			!ce_excluded(dir, repo->index, fullname.buf, ce))
+			continue;
+		if (ce->ce_flags & CE_UPDATE)
+			continue;
+		if (show_cached || show_stage) {
+			if (!show_unmerged || ce_stage(ce))
+				show_ce(repo, dir, ce, fullname.buf,
+					ce_stage(ce) ? tag_unmerged :
+					(ce_skip_worktree(ce) ? tag_skip_worktree :
+						tag_cached));
 		}
+		if (!show_deleted && !show_modified)
+			continue;
+		if (ce_skip_worktree(ce))
+			continue;
+		stat_err = lstat(fullname.buf, &st);
+		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
+			error_errno("cannot lstat '%s'", fullname.buf);
+		if (stat_err && show_deleted)
+			show_ce(repo, dir, ce, fullname.buf, tag_removed);
+		if (show_modified &&
+			(stat_err || ie_modified(repo->index, ce, &st, 0)))
+				show_ce(repo, dir, ce, fullname.buf, tag_modified);
 	}
 
 	strbuf_release(&fullname);
-- 
gitgitgadget

[PATCH v7 3/3] ls-files.c: add --deduplicate option

From: ZheNing Hu via GitGitGadget <hidden>
Date: 2021-01-24 10:55:38

From: ZheNing Hu <redacted>

During a merge conflict, the name of a file may appear multiple
times in "git ls-files" output, once for each stage.  If you use
both `--delete` and `--modify` at the same time, the output may
mention a deleted file twice.

When none of the '-t', '-u', or '-s' options is in use, these
duplicate entries do not add much value to the output.

Introduce a new '--deduplicate' option to suppress them.

Signed-off-by: ZheNing Hu <redacted>
[jc: extended doc and rewritten commit log]
Signed-off-by: Junio C Hamano <redacted>
---
 Documentation/git-ls-files.txt |  8 +++++
 builtin/ls-files.c             | 30 +++++++++++++---
 t/t3012-ls-files-dedup.sh      | 66 ++++++++++++++++++++++++++++++++++
 3 files changed, 100 insertions(+), 4 deletions(-)
 create mode 100755 t/t3012-ls-files-dedup.sh
diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt
index cbcf5263dd0..60449a69b69 100644
--- a/Documentation/git-ls-files.txt
+++ b/Documentation/git-ls-files.txt
@@ -13,6 +13,7 @@ SYNOPSIS
 		(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
 		(-[c|d|o|i|s|u|k|m])*
 		[--eol]
+		[--deduplicate]
 		[-x <pattern>|--exclude=<pattern>]
 		[-X <file>|--exclude-from=<file>]
 		[--exclude-per-directory=<file>]
@@ -81,6 +82,13 @@ OPTIONS
 	\0 line termination on output and do not quote filenames.
 	See OUTPUT below for more information.
 
+--deduplicate::
+	When only filenames are shown, suppress duplicates that may
+	come from having multiple stages during a merge, or giving
+	`--deleted` and `--modified` option at the same time.
+	When any of the `-t`, `--unmerged`, or `--stage` option is
+	in use, this option has no effect.
+
 -x <pattern>::
 --exclude=<pattern>::
 	Skip untracked files matching pattern.
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index 966c0ab0296..fb9cf50d764 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
 static int debug_mode;
 static int show_eol;
 static int recurse_submodules;
+static int skipping_duplicates;
 
 static const char *prefix;
 static int max_prefix_len;
@@ -326,12 +327,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 			continue;
 		if (ce->ce_flags & CE_UPDATE)
 			continue;
-		if (show_cached || show_stage) {
-			if (!show_unmerged || ce_stage(ce))
+		if ((show_cached || show_stage) &&
+			(!show_unmerged || ce_stage(ce))) {
 				show_ce(repo, dir, ce, fullname.buf,
 					ce_stage(ce) ? tag_unmerged :
 					(ce_skip_worktree(ce) ? tag_skip_worktree :
 						tag_cached));
+			if (skipping_duplicates)
+				goto skip_to_next_name;
 		}
 		if (!show_deleted && !show_modified)
 			continue;
@@ -340,11 +343,27 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
 		stat_err = lstat(fullname.buf, &st);
 		if (stat_err && (errno != ENOENT && errno != ENOTDIR))
 			error_errno("cannot lstat '%s'", fullname.buf);
-		if (stat_err && show_deleted)
+		if (stat_err && show_deleted) {
 			show_ce(repo, dir, ce, fullname.buf, tag_removed);
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
 		if (show_modified &&
-			(stat_err || ie_modified(repo->index, ce, &st, 0)))
+			(stat_err || ie_modified(repo->index, ce, &st, 0))) {
 				show_ce(repo, dir, ce, fullname.buf, tag_modified);
+			if (skipping_duplicates)
+				goto skip_to_next_name;
+		}
+		continue;
+skip_to_next_name:
+		{
+			int j;
+			struct cache_entry **cache = repo->index->cache;
+			for (j = i + 1; j < repo->index->cache_nr; j++)
+				if (strcmp(ce->name, cache[j]->name))
+					break;
+			i = j - 1; /* compensate for outer for loop */
+		}
 	}
 
 	strbuf_release(&fullname);
@@ -571,6 +590,7 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 			N_("pretend that paths removed since <tree-ish> are still present")),
 		OPT__ABBREV(&abbrev),
 		OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
+		OPT_BOOL(0,"deduplicate",&skipping_duplicates,N_("suppress duplicate entries")),
 		OPT_END()
 	};
 
@@ -610,6 +630,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
 		 * you also show the stage information.
 		 */
 		show_stage = 1;
+	if (show_tag || show_stage)
+		skipping_duplicates = 0;
 	if (dir.exclude_per_dir)
 		exc_given = 1;
 
diff --git a/t/t3012-ls-files-dedup.sh b/t/t3012-ls-files-dedup.sh
new file mode 100755
index 00000000000..2682b1f43a6
--- /dev/null
+++ b/t/t3012-ls-files-dedup.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='git ls-files --deduplicate test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	>a.txt &&
+	>b.txt &&
+	>delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m base &&
+	echo a >a.txt &&
+	echo b >b.txt &&
+	echo delete >delete.txt &&
+	git add a.txt b.txt delete.txt &&
+	git commit -m tip &&
+	git tag tip &&
+	git reset --hard HEAD^ &&
+	echo change >a.txt &&
+	git commit -a -m side &&
+	git tag side
+'
+
+test_expect_success 'git ls-files --deduplicate to show unique unmerged path' '
+	test_must_fail git merge tip &&
+	git ls-files --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_expect_success 'git ls-files -d -m --deduplicate with different display options' '
+	git reset --hard side &&
+	test_must_fail git merge tip &&
+	rm delete.txt &&
+	git ls-files -d -m --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -t --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	C a.txt
+	C a.txt
+	C a.txt
+	R delete.txt
+	C delete.txt
+	EOF
+	test_cmp expect actual &&
+	git ls-files -d -m -c --deduplicate >actual &&
+	cat >expect <<-\EOF &&
+	a.txt
+	b.txt
+	delete.txt
+	EOF
+	test_cmp expect actual &&
+	git merge --abort
+'
+
+test_done
-- 
gitgitgadget
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help