ls-files.c: add --deduplicate option

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 <adlternative@gmail.com> [jc: extended doc and rewritten commit log] Signed-off-by: Junio C Hamano <gitster@pobox.com>

ZheNing Hu committed Jan 23, 2021 at 10:20 UTC 93a7d9835f008488080d4f61096ee4c12ae60362
3 files changed +102 -3
Documentation/git-ls-files.txt
+8
@@ -13,6 +13,7 @@ SYNOPSIS
13 (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
14 (-[c|d|o|i|s|u|k|m])*
15 [--eol]
16 + [--deduplicate]
17 [-x <pattern>|--exclude=<pattern>]
18 [-X <file>|--exclude-from=<file>]
19 [--exclude-per-directory=<file>]
@@ -80,6 +81,13 @@ OPTIONS
81 \0 line termination on output and do not quote filenames.
82 See OUTPUT below for more information.
83
84 +--deduplicate::
85 + When only filenames are shown, suppress duplicates that may
86 + come from having multiple stages during a merge, or giving
87 + `--deleted` and `--modified` option at the same time.
88 + When any of the `-t`, `--unmerged`, or `--stage` option is
89 + in use, this option has no effect.
90 +
91 -x <pattern>::
92 --exclude=<pattern>::
93 Skip untracked files matching pattern.
builtin/ls-files.c
+28 -3
@@ -35,6 +35,7 @@ static int line_terminator = '\n';
35 static int debug_mode;
36 static int show_eol;
37 static int recurse_submodules;
38 +static int skipping_duplicates;
39
40 static const char *prefix;
41 static int max_prefix_len;
@@ -328,11 +329,14 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
329 if (ce->ce_flags & CE_UPDATE)
330 continue;
331 if ((show_cached || show_stage) &&
331 - (!show_unmerged || ce_stage(ce)))
332 + (!show_unmerged || ce_stage(ce))) {
333 show_ce(repo, dir, ce, fullname.buf,
334 ce_stage(ce) ? tag_unmerged :
335 (ce_skip_worktree(ce) ? tag_skip_worktree :
336 tag_cached));
337 + if (skipping_duplicates)
338 + goto skip_to_next_name;
339 + }
340
341 if (!(show_deleted || show_modified))
342 continue;
@@ -341,11 +345,28 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
345 stat_err = lstat(fullname.buf, &st);
346 if (stat_err && (errno != ENOENT && errno != ENOTDIR))
347 error_errno("cannot lstat '%s'", fullname.buf);
344 - if (stat_err && show_deleted)
348 + if (stat_err && show_deleted) {
349 show_ce(repo, dir, ce, fullname.buf, tag_removed);
350 + if (skipping_duplicates)
351 + goto skip_to_next_name;
352 + }
353 if (show_modified &&
347 - (stat_err || ie_modified(repo->index, ce, &st, 0)))
354 + (stat_err || ie_modified(repo->index, ce, &st, 0))) {
355 show_ce(repo, dir, ce, fullname.buf, tag_modified);
356 + if (skipping_duplicates)
357 + goto skip_to_next_name;
358 + }
359 + continue;
360 +
361 +skip_to_next_name:
362 + {
363 + int j;
364 + struct cache_entry **cache = repo->index->cache;
365 + for (j = i + 1; j < repo->index->cache_nr; j++)
366 + if (strcmp(ce->name, cache[j]->name))
367 + break;
368 + i = j - 1; /* compensate for the for loop */
369 + }
370 }
371
372 strbuf_release(&fullname);
@@ -572,6 +593,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
593 N_("pretend that paths removed since <tree-ish> are still present")),
594 OPT__ABBREV(&abbrev),
595 OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
596 + OPT_BOOL(0, "deduplicate", &skipping_duplicates,
597 + N_("suppress duplicate entries")),
598 OPT_END()
599 };
600
@@ -611,6 +634,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
634 * you also show the stage information.
635 */
636 show_stage = 1;
637 + if (show_tag || show_stage)
638 + skipping_duplicates = 0;
639 if (dir.exclude_per_dir)
640 exc_given = 1;
641
t/t3012-ls-files-dedup.sh new
+66
@@ -0,0 +1,66 @@
1 +#!/bin/sh
2 +
3 +test_description='git ls-files --deduplicate test'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success 'setup' '
8 + >a.txt &&
9 + >b.txt &&
10 + >delete.txt &&
11 + git add a.txt b.txt delete.txt &&
12 + git commit -m base &&
13 + echo a >a.txt &&
14 + echo b >b.txt &&
15 + echo delete >delete.txt &&
16 + git add a.txt b.txt delete.txt &&
17 + git commit -m tip &&
18 + git tag tip &&
19 + git reset --hard HEAD^ &&
20 + echo change >a.txt &&
21 + git commit -a -m side &&
22 + git tag side
23 +'
24 +
25 +test_expect_success 'git ls-files --deduplicate to show unique unmerged path' '
26 + test_must_fail git merge tip &&
27 + git ls-files --deduplicate >actual &&
28 + cat >expect <<-\EOF &&
29 + a.txt
30 + b.txt
31 + delete.txt
32 + EOF
33 + test_cmp expect actual &&
34 + git merge --abort
35 +'
36 +
37 +test_expect_success 'git ls-files -d -m --deduplicate with different display options' '
38 + git reset --hard side &&
39 + test_must_fail git merge tip &&
40 + rm delete.txt &&
41 + git ls-files -d -m --deduplicate >actual &&
42 + cat >expect <<-\EOF &&
43 + a.txt
44 + delete.txt
45 + EOF
46 + test_cmp expect actual &&
47 + git ls-files -d -m -t --deduplicate >actual &&
48 + cat >expect <<-\EOF &&
49 + C a.txt
50 + C a.txt
51 + C a.txt
52 + R delete.txt
53 + C delete.txt
54 + EOF
55 + test_cmp expect actual &&
56 + git ls-files -d -m -c --deduplicate >actual &&
57 + cat >expect <<-\EOF &&
58 + a.txt
59 + b.txt
60 + delete.txt
61 + EOF
62 + test_cmp expect actual &&
63 + git merge --abort
64 +'
65 +
66 +test_done