From f1c462ea4119f58a230abd62096a174483930a18 Mon Sep 17 00:00:00 2001 From: ZheNing Hu Date: Sat, 23 Jan 2021 10:20:08 +0000 Subject: [PATCH 1/3] ls_files.c: bugfix for --deleted and --modified 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 [jc: fixed misindented code] Signed-off-by: Junio C Hamano --- 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 c8eae899b82a83..ce6f6ad00ed9ae 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); } } From ed644d1666415cbed14cc6cd17a658bb56e7f28b Mon Sep 17 00:00:00 2001 From: ZheNing Hu Date: Sat, 23 Jan 2021 10:20:09 +0000 Subject: [PATCH 2/3] ls_files.c: consolidate two for loops into one This will make it easier to show only one entry per filename in the next step. Signed-off-by: ZheNing Hu [jc: corrected the log message] Signed-off-by: Junio C Hamano --- 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 ce6f6ad00ed9ae..e94d724aff79c6 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); From 93a7d9835f008488080d4f61096ee4c12ae60362 Mon Sep 17 00:00:00 2001 From: ZheNing Hu Date: Sat, 23 Jan 2021 10:20:10 +0000 Subject: [PATCH 3/3] 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 [jc: extended doc and rewritten commit log] Signed-off-by: Junio C Hamano --- 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 0a3b5265b34579..6d11ab506b7c2e 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 |--exclude=] [-X |--exclude-from=] [--exclude-per-directory=] @@ -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 :: --exclude=:: Skip untracked files matching pattern. diff --git a/builtin/ls-files.c b/builtin/ls-files.c index e94d724aff79c6..f6f9e483b27e18 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 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 00000000000000..2682b1f43a6665 --- /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