Merge branch 'zh/ls-files-deduplicate'

"git ls-files" can and does show multiple entries when the index is
unmerged, which is a source for confusion unless -s/-u option is in
use.  A new option --deduplicate has been introduced.

* zh/ls-files-deduplicate:
  ls-files.c: add --deduplicate option
  ls_files.c: consolidate two for loops into one
  ls_files.c: bugfix for --deleted and --modified
This commit is contained in:
Junio C Hamano 2021-02-05 16:40:44 -08:00
commit 5198426d91
3 changed files with 123 additions and 30 deletions

View file

@ -13,6 +13,7 @@ SYNOPSIS
(--[cached|deleted|others|ignored|stage|unmerged|killed|modified])* (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])*
(-[c|d|o|i|s|u|k|m])* (-[c|d|o|i|s|u|k|m])*
[--eol] [--eol]
[--deduplicate]
[-x <pattern>|--exclude=<pattern>] [-x <pattern>|--exclude=<pattern>]
[-X <file>|--exclude-from=<file>] [-X <file>|--exclude-from=<file>]
[--exclude-per-directory=<file>] [--exclude-per-directory=<file>]
@ -80,6 +81,13 @@ OPTIONS
\0 line termination on output and do not quote filenames. \0 line termination on output and do not quote filenames.
See OUTPUT below for more information. 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>:: -x <pattern>::
--exclude=<pattern>:: --exclude=<pattern>::
Skip untracked files matching pattern. Skip untracked files matching pattern.

View file

@ -35,6 +35,7 @@ static int line_terminator = '\n';
static int debug_mode; static int debug_mode;
static int show_eol; static int show_eol;
static int recurse_submodules; static int recurse_submodules;
static int skipping_duplicates;
static const char *prefix; static const char *prefix;
static int max_prefix_len; static int max_prefix_len;
@ -312,45 +313,59 @@ static void show_files(struct repository *repo, struct dir_struct *dir)
if (show_killed) if (show_killed)
show_killed_files(repo->index, dir); 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) && construct_fullname(&fullname, repo, ce);
!ce_excluded(dir, repo->index, fullname.buf, ce))
continue; if ((dir->flags & DIR_SHOW_IGNORED) &&
if (show_unmerged && !ce_stage(ce)) !ce_excluded(dir, repo->index, fullname.buf, ce))
continue; continue;
if (ce->ce_flags & CE_UPDATE) if (ce->ce_flags & CE_UPDATE)
continue; continue;
if ((show_cached || show_stage) &&
(!show_unmerged || ce_stage(ce))) {
show_ce(repo, dir, ce, fullname.buf, show_ce(repo, dir, ce, fullname.buf,
ce_stage(ce) ? tag_unmerged : ce_stage(ce) ? tag_unmerged :
(ce_skip_worktree(ce) ? tag_skip_worktree : (ce_skip_worktree(ce) ? tag_skip_worktree :
tag_cached)); tag_cached));
if (skipping_duplicates)
goto skip_to_next_name;
} }
}
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 (!(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 (skipping_duplicates)
goto skip_to_next_name;
}
if (show_modified &&
(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;
if ((dir->flags & DIR_SHOW_IGNORED) && skip_to_next_name:
!ce_excluded(dir, repo->index, fullname.buf, ce)) {
continue; int j;
if (ce->ce_flags & CE_UPDATE) struct cache_entry **cache = repo->index->cache;
continue; for (j = i + 1; j < repo->index->cache_nr; j++)
if (ce_skip_worktree(ce)) if (strcmp(ce->name, cache[j]->name))
continue; break;
err = lstat(fullname.buf, &st); i = j - 1; /* compensate for the for loop */
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 +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")), N_("pretend that paths removed since <tree-ish> are still present")),
OPT__ABBREV(&abbrev), OPT__ABBREV(&abbrev),
OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")), OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")),
OPT_BOOL(0, "deduplicate", &skipping_duplicates,
N_("suppress duplicate entries")),
OPT_END() OPT_END()
}; };
@ -617,6 +634,8 @@ int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix)
* you also show the stage information. * you also show the stage information.
*/ */
show_stage = 1; show_stage = 1;
if (show_tag || show_stage)
skipping_duplicates = 0;
if (dir.exclude_per_dir) if (dir.exclude_per_dir)
exc_given = 1; exc_given = 1;

66
t/t3012-ls-files-dedup.sh Executable file
View file

@ -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