From: Mirko Faina <mroik@delayed.space>
To: git@vger.kernel.org
Cc: Mirko Faina <mroik@delayed.space>, Jeff King <peff@peff.net>,
Junio C Hamano <gitster@pobox.com>,
Elijah Newren <newren@gmail.com>,
Derrick Stolee <stolee@gmail.com>
Subject: [RFC PATCH 6/6] builtin/ls-files.c: support for precious files
Date: Thu, 8 Oct 2026 14:07:02 +0200 [thread overview]
Message-ID: <f5abaac0ff3b1d4598fa27bbcf338e1a36a08098.1791460418.git.mroik@delayed.space> (raw)
In-Reply-To: <cover.1791460418.git.mroik@delayed.space>
Teach "git ls-files" new variants of "-i" with --ignored=trashable
and --ignored=precious, where git shows only trashable files with the
first and only precious files with the second. If no optional argument
is passed "ls-files" retains the current behaviour.
Signed-off-by: Mirko Faina <mroik@delayed.space>
---
builtin/ls-files.c | 73 ++++++++++++++++++++++++++---
dir.c | 74 ++++++++++++++++++++++++++----
dir.h | 34 +++++++++++++-
t/t2205-add-worktree-config.sh | 2 +-
t/t3001-ls-files-others-exclude.sh | 27 +++++++++++
5 files changed, 190 insertions(+), 20 deletions(-)
diff --git a/builtin/ls-files.c b/builtin/ls-files.c
index d1cc7e92f4..63dd769385 100644
--- a/builtin/ls-files.c
+++ b/builtin/ls-files.c
@@ -163,8 +163,21 @@ static void show_dir_entry(struct index_state *istate,
write_name(ent->name);
}
-static void show_other_files(struct index_state *istate,
- const struct dir_struct *dir)
+static void show_precious_files(struct index_state *istate,
+ const struct dir_struct *dir)
+{
+ int i;
+
+ for (i = 0; i < dir->precious_nr; i++) {
+ struct dir_entry *ent = dir->precious[i];
+ if (!index_name_is_other(istate, ent->name, ent->len))
+ continue;
+ show_dir_entry(istate, tag_other, ent);
+ }
+}
+
+static void show_entries_files(struct index_state *istate,
+ const struct dir_struct *dir)
{
int i;
@@ -176,6 +189,24 @@ static void show_other_files(struct index_state *istate,
}
}
+static void show_ignored_files(struct index_state *istate,
+ const struct dir_struct *dir)
+{
+ show_entries_files(istate, dir);
+ show_precious_files(istate, dir);
+}
+
+static void show_other_files(struct index_state *istate,
+ const struct dir_struct *dir)
+{
+ if (dir->flags & DIR_SHOW_IGNORED)
+ show_ignored_files(istate, dir);
+ else if (dir->flags & DIR_SHOW_PRECIOUS)
+ show_precious_files(istate, dir);
+ else
+ show_entries_files(istate, dir);
+}
+
static void show_killed_files(struct index_state *istate,
const struct dir_struct *dir)
{
@@ -547,6 +578,30 @@ static const char * const ls_files_usage[] = {
NULL
};
+static int option_parse_ignored(const struct option *opt, const char *arg, int unset)
+{
+ enum dir_struct_flags *flags = opt->value;
+
+ BUG_ON_OPT_NEG(unset);
+
+ /*
+ * Reset in case --ignored=<type> is specified multiple times, we keep
+ * the last one.
+ */
+ *flags &= ~(DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS);
+
+ if (!arg)
+ *flags |= DIR_SHOW_IGNORED;
+ else if (!strcmp(arg, "trashable"))
+ *flags |= DIR_SHOW_TRASHABLE;
+ else if (!strcmp(arg, "precious"))
+ *flags |= DIR_SHOW_PRECIOUS;
+ else
+ die(_("'%s' argument is not a valid value"), arg);
+
+ return 0;
+}
+
static int option_parse_exclude(const struct option *opt,
const char *arg, int unset)
{
@@ -615,9 +670,9 @@ int cmd_ls_files(int argc,
N_("show modified files in the output")),
OPT_BOOL('o', "others", &show_others,
N_("show other files in the output")),
- OPT_BIT('i', "ignored", &dir.flags,
+ OPT_CALLBACK_F('i', "ignored", &dir.flags, N_("type"),
N_("show ignored files in the output"),
- DIR_SHOW_IGNORED),
+ PARSE_OPT_OPTARG | PARSE_OPT_NONEG, option_parse_ignored),
OPT_BOOL('s', "stage", &show_stage,
N_("show staged contents' object name in the output")),
OPT_BOOL('k', "killed", &show_killed,
@@ -704,7 +759,9 @@ int cmd_ls_files(int argc,
tag_skip_worktree = "S ";
tag_resolve_undo = "U ";
}
- if (show_modified || show_others || show_deleted || (dir.flags & DIR_SHOW_IGNORED) || show_killed)
+ if (show_modified || show_others || show_deleted ||
+ (dir.flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) ||
+ show_killed)
require_work_tree = 1;
if (show_unmerged)
/*
@@ -753,10 +810,12 @@ int cmd_ls_files(int argc,
if (pathspec.nr && error_unmatch)
ps_matched = xcalloc(pathspec.nr, 1);
- if ((dir.flags & DIR_SHOW_IGNORED) && !show_others && !show_cached)
+ if ((dir.flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) &&
+ !show_others && !show_cached)
die("ls-files -i must be used with either -o or -c");
- if ((dir.flags & DIR_SHOW_IGNORED) && !exc_given)
+ if ((dir.flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) &&
+ !exc_given)
die("ls-files --ignored needs some exclude pattern");
/* With no flags, we default to showing the cached files */
diff --git a/dir.c b/dir.c
index ffc1818533..274a934e12 100644
--- a/dir.c
+++ b/dir.c
@@ -281,10 +281,25 @@ int fill_directory(struct dir_struct *dir,
{
const char *matched_prefix;
size_t prefix_len;
+ unsigned exclusive;
+ int i;
- unsigned exclusive_flags = DIR_SHOW_IGNORED | DIR_SHOW_IGNORED_TOO;
- if ((dir->flags & exclusive_flags) == exclusive_flags)
- BUG("DIR_SHOW_IGNORED and DIR_SHOW_IGNORED_TOO are exclusive");
+ const unsigned exclusive_with_too[] = {
+ DIR_SHOW_IGNORED,
+ DIR_SHOW_TRASHABLE,
+ DIR_SHOW_PRECIOUS,
+ };
+ const char *exclusive_bug_text[] = {
+ "DIR_SHOW_IGNORED",
+ "DIR_SHOW_TRASHABLE",
+ "DIR_SHOW_PRECIOUS",
+ };
+ for (i = 0; i < 3; i++) {
+ exclusive = (exclusive_with_too[i] | DIR_SHOW_IGNORED_TOO);
+ if ((dir->flags & exclusive) == exclusive)
+ BUG("%s and DIR_SHOW_IGNORED_TOO are exclusive",
+ exclusive_bug_text[i]);
+ }
/*
* Calculate common prefix for the pathspec, and
@@ -1859,6 +1874,21 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir,
basename, dtype_p);
}
+/*
+ * Loads the exclude lists for the directory containing pathname, then
+ * scans all exclude lists to determine whether pathname is precious.
+ * Returns 1 if true, otherwise 0.
+ */
+int is_precious(struct dir_struct *dir, struct index_state *istate,
+ const char *pathname, int *dtype_p)
+{
+ struct path_pattern *pattern =
+ last_matching_pattern(dir, istate, pathname, dtype_p);
+ if (pattern)
+ return !!(pattern->flags & PATTERN_FLAG_PRECIOUS);
+ return 0;
+}
+
/*
* Loads the exclude lists for the directory containing pathname, then
* scans all exclude lists to determine whether pathname is trashable.
@@ -1911,6 +1941,17 @@ static struct dir_entry *dir_add_name(struct dir_struct *dir,
return dir->entries[dir->nr++] = dir_entry_new(pathname, len);
}
+static struct dir_entry *dir_add_precious(struct dir_struct *dir,
+ struct index_state *istate,
+ const char *pathname, int len)
+{
+ if (index_file_exists(istate, pathname, len, repo_ignore_case(the_repository)))
+ return NULL;
+
+ ALLOC_GROW(dir->precious, dir->precious_nr+1, dir->internal.precious_alloc);
+ return dir->precious[dir->precious_nr++] = dir_entry_new(pathname, len);
+}
+
struct dir_entry *dir_add_ignored(struct dir_struct *dir,
struct index_state *istate,
const char *pathname, int len)
@@ -2528,13 +2569,18 @@ static enum path_treatment treat_path(struct dir_struct *dir,
(directory_exists_in_index(istate, path->buf, path->len) == index_nonexistent))
return path_none;
- excluded = is_excluded(dir, istate, path->buf, &dtype);
+ excluded = is_excluded(dir, istate, path->buf, &dtype) ||
+ is_trashable(dir, istate, path->buf, &dtype) ||
+ is_precious(dir, istate, path->buf, &dtype);
/*
* Excluded? If we don't explicitly want to show
* ignored files, ignore it
*/
- if (excluded && !(dir->flags & (DIR_SHOW_IGNORED|DIR_SHOW_IGNORED_TOO)))
+ if (excluded && !(dir->flags & (DIR_SHOW_IGNORED|
+ DIR_SHOW_TRASHABLE|
+ DIR_SHOW_PRECIOUS|
+ DIR_SHOW_IGNORED_TOO)))
return path_excluded;
switch (dtype) {
@@ -2704,20 +2750,28 @@ static void add_path_to_appropriate_result_list(struct dir_struct *dir,
const struct pathspec *pathspec,
enum path_treatment state)
{
+ int dtype;
+
/* add the path to the appropriate result list */
switch (state) {
case path_excluded:
- if (dir->flags & DIR_SHOW_IGNORED)
- dir_add_name(dir, istate, path->buf, path->len);
- else if ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
+ if (dir->flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) {
+ dtype = resolve_dtype(cdir->d_type, istate, path->buf, path->len);
+ if (is_precious(dir, istate, path->buf, &dtype))
+ dir_add_precious(dir, istate, path->buf, path->len);
+ else
+ dir_add_name(dir, istate, path->buf, path->len);
+ } else if ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
((dir->flags & DIR_COLLECT_IGNORED) &&
exclude_matches_pathspec(path->buf, path->len,
- pathspec)))
+ pathspec))) {
dir_add_ignored(dir, istate, path->buf, path->len);
+ }
break;
case path_untracked:
- if (dir->flags & DIR_SHOW_IGNORED)
+ if (dir->flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE |
+ DIR_SHOW_PRECIOUS))
break;
dir_add_name(dir, istate, path->buf, path->len);
if (cdir->fdir)
diff --git a/dir.h b/dir.h
index 5424036e4e..58e5467cda 100644
--- a/dir.h
+++ b/dir.h
@@ -212,14 +212,30 @@ struct untracked_cache {
struct dir_struct {
/* bit-field of options */
- enum {
+ enum dir_struct_flags {
/**
* Return just ignored files in `entries[]`, not untracked files.
* This flag is mutually exclusive with `DIR_SHOW_IGNORED_TOO`.
+ * This is a superset of DIR_SHOW_TRASHABLE and DIR_SHOW_PRECIOUS.
+ *
+ * This flag should probably be dropped throughout the codebase
+ * in favour of (DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS).
*/
DIR_SHOW_IGNORED = 1<<0,
+ /**
+ * Return just trashable files in `entries[]`, not untracked files.
+ * This flag is mutually exclusive with `DIR_SHOW_IGNORED_TOO`.
+ */
+ DIR_SHOW_TRASHABLE = 1<<10,
+
+ /**
+ * Return just precious files in `entries[]`, not untracked files.
+ * This flag is mutually exclusive with `DIR_SHOW_IGNORED_TOO`.
+ */
+ DIR_SHOW_PRECIOUS = 1<<11,
+
/* Include a directory that is not tracked. */
DIR_SHOW_OTHER_DIRECTORIES = 1<<1,
@@ -243,7 +259,8 @@ struct dir_struct {
/**
* Similar to `DIR_SHOW_IGNORED`, but return ignored files in
* `ignored[]` in addition to untracked files in `entries[]`.
- * This flag is mutually exclusive with `DIR_SHOW_IGNORED`.
+ * This flag is mutually exclusive with `DIR_SHOW_IGNORED`,
+ * `DIR_SHOW_TRASHABLE` and `DIR_SHOW_PRECIOUS`.
*/
DIR_SHOW_IGNORED_TOO = 1<<5,
@@ -278,12 +295,18 @@ struct dir_struct {
/* The number of members in `entries[]` array. */
int nr; /* output only */
+ /* The number of members in `precious[]` array. */
+ int precious_nr; /* output only */
+
/* The number of members in `ignored[]` array. */
int ignored_nr; /* output only */
/* An array of `struct dir_entry`, each element of which describes a path. */
struct dir_entry **entries; /* output only */
+ /* Used for listing precious files with `DIR_SHOW_PRECIOUS`. */
+ struct dir_entry **precious; /* output only */
+
/**
* used for ignored paths with the `DIR_SHOW_IGNORED_TOO` and
* `DIR_COLLECT_IGNORED` flags.
@@ -307,6 +330,9 @@ struct dir_struct {
/* Keeps track of allocation of `entries[]` array.*/
int alloc;
+ /* Keeps track of allocation of `precious[]` array.*/
+ int precious_alloc;
+
/* Keeps track of allocation of `ignored[]` array. */
int ignored_alloc;
@@ -442,6 +468,10 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir,
struct index_state *istate,
const char *name, int *dtype);
+int is_precious(struct dir_struct *dir,
+ struct index_state *istate,
+ const char *name, int *dtype);
+
int is_trashable(struct dir_struct *dir,
struct index_state *istate,
const char *name, int *dtype);
diff --git a/t/t2205-add-worktree-config.sh b/t/t2205-add-worktree-config.sh
index 43d950de64..a743470196 100755
--- a/t/t2205-add-worktree-config.sh
+++ b/t/t2205-add-worktree-config.sh
@@ -244,7 +244,7 @@ test_expect_success '3a: setup--add repo dir' '
test_expect_success '3b: ignored' '
(
cd test3 &&
- git --git-dir=repo/.git ls-files -io --directory --exclude-standard >actual-ignored-unsorted &&
+ git --git-dir=repo/.git ls-files -i -o --directory --exclude-standard >actual-ignored-unsorted &&
sort actual-ignored-unsorted >actual-ignored &&
sort expect-ignored-unsorted >expect-ignored &&
test_cmp expect-ignored actual-ignored
diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh
index 29a0a25b30..977cbd6b6e 100755
--- a/t/t3001-ls-files-others-exclude.sh
+++ b/t/t3001-ls-files-others-exclude.sh
@@ -55,6 +55,7 @@ expect
!*.8' >.git/ignore
echo '*.1
+$wasder
/*.3
!*.6' >.gitignore
echo '*.2
@@ -76,6 +77,32 @@ test_expect_success 'git ls-files --others with various exclude options.' '
test_cmp expect output
'
+test_expect_success 'git ls-files -o -i' '
+ touch wasder &&
+ git ls-files -o -i --exclude-standard >output &&
+ cat output &&
+ test_grep "wasder" output
+'
+
+test_expect_success 'git ls-files -o --ignored=trashable' '
+ git ls-files -o --ignored=trashable --exclude-standard >output &&
+ cat output &&
+ test_grep ! "wasder" output
+'
+
+test_expect_success 'git ls-files -o --ignored=precious' '
+ git ls-files -o --ignored=precious --exclude-standard >output &&
+ cat output &&
+ test_grep "wasder" output
+'
+
+test_expect_success 'git ls-files -o --ignored=trashable' '
+ test_when_finished rm wasder &&
+ git ls-files -o --ignored=precious --ignored=trashable --exclude-standard >output &&
+ cat output &&
+ test_grep ! "wasder" output
+'
+
# Test \r\n (MSDOS-like systems)
printf '*.1\r\n/*.3\r\n!*.6\r\n' >.gitignore
--
2.56.0
next prev parent reply other threads:[~2026-10-08 12:07 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-08 12:06 [RFC PATCH 0/6] Introduce precious files Mirko Faina
2026-10-08 12:06 ` [RFC PATCH 1/6] precious-files.txt: new document proposing new precious file type Mirko Faina
2026-10-08 12:06 ` [RFC PATCH 2/6] dir.h: replace pattern macros with enum in attr.h Mirko Faina
2026-10-08 12:06 ` [RFC PATCH 3/6] dir.c: teach parse_path_pattern() precious files Mirko Faina
2026-10-08 12:07 ` [RFC PATCH 4/6] dir.c: teach add_pattern() reject precious pattern Mirko Faina
2026-10-08 12:07 ` [RFC PATCH 5/6] unpack-trees: teach check_ok_to_remove() precious Mirko Faina
2026-10-08 12:07 ` Mirko Faina [this message]
2026-10-08 12:12 ` [RFC PATCH 0/6] Introduce precious files Mirko Faina
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=f5abaac0ff3b1d4598fa27bbcf338e1a36a08098.1791460418.git.mroik@delayed.space \
--to=mroik@delayed.space \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=newren@gmail.com \
--cc=peff@peff.net \
--cc=stolee@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox