{"thread":{"id":"66490","subject":"[RFC PATCH 0/6] Introduce precious files","startedAt":"2026-10-08T12:06:56Z","lastAt":"2026-10-08T12:12:47Z","messageCount":8,"participants":["Mirko Faina"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"554487","messageId":"cover.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":null,"subject":"[RFC PATCH 0/6] Introduce precious files","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:06:56Z","receivedAt":"2026-10-08T12:06:56Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"Introduce precious files based on Elijah Newren's document [1].\n\nWith this series documentation is not updated yet but I wanted some feedback\nbefore polishing and moving on to implement the changes for \"git clean\".\n\n[1] https://lore.kernel.org/git/pull.1627.git.1703643931314.gitgitgadget@gmail.com/\n\n[1/6] precious-files.txt: new document proposing new precious file type (Elijah Newren)\n[2/6] dir.h: replace pattern macros with enum in attr.h (Mirko Faina)\n[3/6] dir.c: teach parse_path_pattern() precious files (Mirko Faina)\n[4/6] dir.c: teach add_pattern() reject precious pattern (Mirko Faina)\n[5/6] unpack-trees: teach check_ok_to_remove() precious (Mirko Faina)\n[6/6] builtin/ls-files.c: support for precious files (Mirko Faina)\n\n Documentation/technical/precious-files.txt | 540 +++++++++++++++++++++\n attr.c                                     |   8 +-\n attr.h                                     |  10 +-\n builtin/check-ignore.c                     |   1 +\n builtin/clean.c                            |   4 +-\n builtin/ls-files.c                         |  75 ++-\n builtin/sparse-checkout.c                  |  16 +-\n dir.c                                      | 144 +++++-\n dir.h                                      |  51 +-\n t/helper/test-path-walk.c                  |   2 +-\n t/t1091-sparse-checkout-builtin.sh         |   8 +\n t/t2205-add-worktree-config.sh             |   2 +-\n t/t3001-ls-files-others-exclude.sh         |  27 ++\n t/t7508-status.sh                          |   9 +\n unpack-trees.c                             |   7 +-\n 15 files changed, 849 insertions(+), 55 deletions(-)\n create mode 100644 Documentation/technical/precious-files.txt\n\n-- \n2.56.0\n\n\n"},{"id":"554488","messageId":"b169f19456a7ba66d86df183a495e6f9f28ad5b9.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"[RFC PATCH 1/6] precious-files.txt: new document proposing new precious file type","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:06:57Z","receivedAt":"2026-10-08T12:06:57Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"From: Elijah Newren <newren@gmail.com>\n\nWe have traditionally considered all ignored files to be expendable, but\nusers occasionally want ignored files that are not considered\nexpendable.  Add a design document covering how to split ignored files\ninto two types: 'trashable' (what all ignored files are currently\nconsidered) and 'precious' (the new type of ignored file).\n\nHelped-by: Sebastian Thiel <sebastian.thiel@icloud.com>\nSigned-off-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Mirko Faina <mroik@delayed.space>\n---\n  Was taken as is from [1]\n\n  [1] https://lore.kernel.org/git/pull.1627.git.1703643931314.gitgitgadget@gmail.com/\n\n Documentation/technical/precious-files.txt | 540 +++++++++++++++++++++\n 1 file changed, 540 insertions(+)\n create mode 100644 Documentation/technical/precious-files.txt\n\ndiff --git a/Documentation/technical/precious-files.txt b/Documentation/technical/precious-files.txt\nnew file mode 100644\nindex 0000000000..05c205b57b\n--- /dev/null\n+++ b/Documentation/technical/precious-files.txt\n@@ -0,0 +1,540 @@\n+Precious Files Design Document\n+==============================\n+\n+Table of Contents\n+  * Objective\n+  * Background\n+    * File categorization exceptions\n+  * Proposal\n+    * Precious file specification\n+    * Breakdown of suggested behaviors by command\n+  * Backward compatibility notes\n+    * Slightly incompatible syntax\n+    * Interaction with sparse-checkout parsing\n+    * Behavior of traditional flags\n+    * Interaction with older Git clients\n+    * Commands with modified meaning\n+  * Implementation hints\n+    * Data structures\n+    * Code areas\n+    * Minimum\n+  * Out of scope\n+  * Previous discussions\n+  * Alternatives considered\n+\n+Objective\n+---------\n+Support \"Precious\" Files in git, a set of files which are considered\n+ignored (e.g. do not show up in \"git status\" output) but are not expendable\n+(thus won't be removed to make room for a file when switching or merging\n+branches).\n+\n+Background\n+----------\n+In git we have different types of files, with various subdivisions:\n+  * tracked\n+    * present (i.e. part of sparse checkout)\n+    * not present (i.e. not part of sparse checkout)\n+  * not tracked\n+    * ignored (also treated as expendable)\n+    * untracked (more precisely, not-tracked-and-not-ignored, but often\n+      referred to as simply \"untracked\" despite the fact that such a term\n+      is easily mistaken as a synonym to \"not tracked\".  However, we haven't\n+      been fully consistent, and some places like `git ls-files --others`\n+      may use \"untracked\" to refer to the larger not-tracked category).\n+      Not considered expendable.\n+\n+Over the years, the fact that ignored files are unconditionally treated as\n+expendable (so that other operations like git checkout might wipe them out\n+to make room for files on the other branch) has occasionally caused\n+problems.  Many have expressed a desire for subdividing the ignored class,\n+so that we have both ignored-and-expendable (possibly referred to as\n+\"trashable\", covering the only type of ignored file we have today) and\n+introducing ignored-and-not-expendable (often referred to as \"precious\").\n+\n+File categorization exceptions\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+Our division above into nice categories is actually a bit of a lie.\n+\n+Once upon a time untracked files were considered expendable[1].  Even after\n+that changed, we still had lots of edge cases where untracked files were\n+deleted when they shouldn't be, and ignored files weren't deleted when they\n+should be[2].  While that has been (mostly) fixed, despite the general\n+intent to preserve untracked files, we have special cases that are\n+documented as not preserving them[4,5].  There are also a few codepaths\n+that have comments about locations that might (or definitely do)\n+erroneously delete untracked paths[6].  And at least one code path that is\n+known to erroneously delete untracked paths which has not been commented:\n+`git checkout <tree> <pathspec>`.  And there may be more.\n+\n+[1] https://lore.kernel.org/git/CABPp-BFyR19ch71W10oJDFuRX1OHzQ3si971pMn6dPtHKxJDXQ@mail.gmail.com/\n+[2] https://lore.kernel.org/git/pull.1036.v3.git.1632760428.gitgitgadget@gmail.com/\n+[3] https://lore.kernel.org/git/de416f887d7ce24f20ad3ad4cc838394d6523635.1632760428.git.gitgitgadget@gmail.com/\n+[4] https://lore.kernel.org/git/xmqqr1e2ejs9.fsf@gitster.g/\n+[5] https://lore.kernel.org/git/de416f887d7ce24f20ad3ad4cc838394d6523635.1632760428.git.gitgitgadget@gmail.com/\n+[6] https://lore.kernel.org/git/6b42a80bf3d46e16980d0724e8b07101225239d0.1632760428.git.gitgitgadget@gmail.com/\n+\n+This history and these exceptions matter to this proposal because:\n+  * it highlights how much work can be involved in trying to treat a class\n+    of files as not expendable\n+  * the existing corner cases where untracked files are erroneously\n+    treated as expendable will probably also double as corner cases where\n+    precious files are treated as expendable\n+  * the past fixes for treating untracked files as precious will likely\n+    highlight the needed types of code changes to treat ignored files as\n+    precious\n+\n+Proposal\n+--------\n+We propose adding another class of files: ignored-but-not-expendable,\n+referred to by the shorthand of \"precious\".  The proposal is simple at a\n+high level, but there are many details to consider:\n+  * How to specify precious files (extended .gitignore syntax?  attributes?)\n+  * Which commands should be modified, and how?\n+  * How to handle flags that are essentially a partial implementation of\n+    a precious capability (e.g. [--[no-]overwrite-ignore])?\n+  * How will older Git clients behave on a repo with precious files?\n+The subsequent sections will try to address these questions in more detail.\n+\n+One thing to highlight here is that the class formerly called\n+`ignored` now has two subtypes: (1) the type we already have,\n+ignored-and-expendable (sometimes referred to below as \"trashable\")\n+and (2) the new type, ignored-and-not-expendable (referred to as\n+\"precious\").\n+\n+Precious file specification\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+As per [P2]:\n+\n+    \"\"\"\n+    Even though I referred to the precious _attribute_ in some of these\n+    discussions, between the attribute mechanism and the ignore\n+    mechanism, I am actually leaning toward suggesting to extend the\n+    exclude/ignore mechanism to introduce the \"precious\" class.  That\n+    way, we can avoid possible snafu arising from marking a path in\n+    .gitignore as ignored, and in .gitattrbutes as precious, and have to\n+    figure out how these two settings are to work together.\n+    \"\"\"\n+\n+we specify precious files via an extension to .gitignore.  In particular,\n+lines starting with a '$' character specify that the file is precious.\n+For example:\n+  $.config\n+would say the file `.config` is precious.\n+\n+Now that there are three types of files specified by .gitignore files --\n+untracked, trashable (ignored-and-expendable), and precious\n+(ignored-and-not-expendable), the meaning of `!` at the begining of a line\n+needs careful clarification.  It could be seen as \"not ignored\" or as \"not\n+trashable\", given the subdivision of ignored files that has occurred.  We\n+specifically take it to mean \"not ignored\", i.e. \"untracked\".\n+\n+This leaves us with a simple set of rules to provide to users about lines\n+in their '.gitignore' file:\n+  * No special prefix character => ignored-and-expendable (\"trashable\")\n+  * A '$' prefix character      => ignored-and-not-expendable (\"precious\")\n+  * A '!' prefix character      => not ignored, i.e. untracked\n+\n+It's worth noting that the traditional use of '!' as a negation\n+character needs updating, given the introduction of a ternary state\n+(\"not trashable\" could mean either untracked or precious, which is\n+ambiguous).  Refrain from referring to '!' as a negation character to\n+avoid confusion.  To assist users in making this mindset shift, flag\n+any line beginning with '!$' as an error. As always,\n+backslash-escaping remains an option, allowing users to specify\n+entries like '!\\$foo' to mark a file named '$foo' as untracked.\n+\n+Breakdown of suggested behaviors by command\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+See also \"Out of Scope\" section below, particularly for:\n+  * apply, am [without -3]\n+  * checkout/restore\n+  * checkout-index\n+  * additional information on merge backends\n+\n+Documentation:\n+  * audit for references to \"ignore\" and \"ignored\", to see which ones need\n+    to now replace those with either \"ignored-and-expendable\" (or\n+    \"trashable\"), and which can remain \"ignored\".\n+  * audit for \"exclude\" and \"excluded\" (the older terminology for ignored\n+    files) and update them as well.\n+  * add references to \"precious\" (and perhaps \"trashable) as needed (don't\n+    forget the glossary)\n+  * rm: update the documentation:\n+      \"Ignored files are deemed expendable and won't stop\" ->\n+      \"Ignored files, unless specifically marked precious, are by default\n+       deemed expendable and won't stop\"\n+  * ensure all codepaths touched by 0e29222e0c2 (\"Documentation: call out\n+    commands that nuke untracked files/directories\", 2021-09-27) also call\n+    out that they'll nuke precious files in addition to untracked ones.\n+  * change the documentation for '!' in gitignore to stop using the term\n+    'negates'; it's potentially misleading now (negating a ternary value\n+    yields an ambiguous value).  Instead, the prefix is used to mark\n+    untracked (or \"not ignored\") files.\n+  * note that the --[no-]overwrite-ignore option is deprecated, and, since\n+    it predated the introduction of precious files is also a misnomer.  The\n+    correct name of the option would actually be --[no-]overwrite-trashable\n+    but it is too late to rename.\n+  * consider documenting that merge's --no-overwrite-ignore option is\n+    virtually worthless (only works with the fast-forwarding backend).\n+  * consider auditing the code for 'untracked' and fixing those to be\n+    'not tracked' in cases where both 'untracked' and 'ignored' files\n+    are meant\n+\n+checkout/switch:\n+  * will need to not overwrite precious files when they are in the way of\n+    switching branches, unless --force/-f is specified.\n+\n+checkout/restore:\n+  * when passed a <tree> as a source, do not overwrite precious files\n+    (NOR untracked files!), unless --force/-f is specified.  [Could be\n+    considered a stretch goal...]\n+\n+merge:\n+  * do not overwrite precious files when they are in the way of merging\n+    branches.  (Must be handled in each and every merge strategy;\n+    user-defined merge strategies may get this wrong.)\n+\n+read-tree:\n+  * -u: do not overwrite precious files when they are in the way, unless...\n+  * --reset and -u: overwrite precious files as well as untracked files.\n+    Add to the warning under --reset about overwritten untracked files to\n+    note that precious files are also overwritten.\n+\n+am -3, cherry-pick, rebase, revert, : same as above for checkout/switch and\n+  merge.\n+\n+add:\n+  * same as today, just make sure when we split the ignored array (ignored &\n+    ignored_nr) into multiple categories that it continues working\n+\n+rm:\n+  * make sure submodules are not removed if precious files are present.\n+    Currently, rm will remove submodules if only ignored files are present.\n+\n+check-ignore:\n+  * since this command exists for debugging gitignore rules, there needs to\n+    be some kind of mechanism for differentiating between trashable and\n+    precious files.  It is okay if this comes with a new command-line flag,\n+    but there should be some tests showing how it behaves both with and\n+    without that flag when precious files are present\n+\n+clean:\n+  * clarify the meaning of -x and -X options: -X now means only remove\n+    trashable files.  -x means remove both untracked and trashable files.\n+    (See also [P17])\n+  * add a --all option for removing all not-tracked files: untracked,\n+    trashable, and precious.\n+  * Other than --all, it is not worth adding flags for cleaning subsets of\n+    not-tracked files that include precious files (thus, no flag for just\n+    precious, or trashable and precious, or untracked and precious)\n+  * Patterns with a leading '$' can be passed to --exclude, if wanted.\n+\n+ls-files:\n+  * --ignored/-i: shows every kind of ignored file (thus behaving the same\n+    as today, since there is no way to distinguish between the types of\n+    ignored in the output)\n+  * add new `--ignored=precious` and `--ignored=trashable` flags for\n+    differentiating.  A plain `--ignored` is like having both\n+    `--ignored=precious` and `--ignored=trashable` specified.\n+  * --exclude,--exclude-from can now take patterns with a leading '$' and\n+    the file will be considered precious rather than trashable.\n+\n+status:\n+  * --ignored (without additional parameters) continues behaving as-is: it\n+    prints both trashable and precious files in its \"Ignored\" category with\n+    no distinguishing.\n+  * --ignored --short will continue showing trashable files with '!!', but\n+    will now show precious files using '$$'.\n+  * --ignored --porcelain={v1,v2} will continue showing precious files\n+    with the '!' character, since scripts may not be prepared to parse a\n+    leading '$'.  We can't break those scripts, even if it'd avoid the\n+    off chance that those scripts act on the information about \"ignored\"\n+    files and end up nuking precious files.\n+  * --ignored --porcelain=v3 will need to be introduced to show precious\n+    files with a leading '$'.\n+\n+sparse-checkout:\n+  * the --rules-file option should be tested with a pattern with a leading\n+    '$' to make sure it prints an expected error.\n+  * it might be worth noting somewhere that sparse-checkout treats\n+    ignored files as precious; when sparsifying, it attempts to remove\n+    directories that do not match the sparse specification, but will\n+    leave them present if any of the tracked files are modified, or if\n+    there are any not-tracked files present.  That includes ignored\n+    files.  That means no additional work is needed for precious\n+    support; I just mention it for completeness.\n+\n+Backward compatibility notes\n+----------------------------\n+There are multiple issues that impinge on backward compatibility (either in\n+terms of special care we need to take, or in terms of messaging we may need\n+to send out about changes):\n+  * Slightly incompatible syntax\n+  * Interaction with sparse-checkout parsing\n+  * Behavior of traditional flags\n+  * Interaction with older Git clients\n+  * Commands with modified meaning\n+We'll discuss each in its own subsection below.\n+\n+Slightly incompatible syntax\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+This new syntax obviously breaks backward compatibility in that an ignored\n+path named `$.config` would now have to be specified as `\\$.config`.  This\n+is similar to how introducing `!` as a prefix in .gitignore files was a\n+backward compatibility break.  We expect and hope that the fallout will be\n+minor.  See also [P10].\n+\n+Interaction with sparse-checkout parsing\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+The $GIT_DIR/info/sparse-checkout file also makes use of gitignore syntax\n+and the gitignore parsing to read the file.  It differs in that the files\n+specified are considered the files to be included (i.e. present in the\n+working copy) rather than which files should be excluded, but otherwise\n+has until now used identical syntax and parsing.\n+\n+However, for sparse-checkout there is no third type of file, so the '$'\n+prefix makes no sense for it.  As such, it should be an error for any\n+lines to begin with '$' in a sparse-checkout file.\n+\n+(This also means that if anyone really did have a path beginning with '$'\n+in sparse-checkout files previously, then they now need to backslash escape\n+them, the same as with .gitignore files.)\n+\n+While we could theoretically avoid this small backward compatibility break\n+for sparse-checkout parsing by just treating a leading '$' the way it\n+traditionally has been done, I am worried about practically maintaining that\n+solution:\n+  * the gitignore parsing is peppered with references like 'exclude' that\n+    are specific to the gitignore case\n+  * because of the above, it is _heavily_ confusing to attempt to read and\n+    understand the gitignore handling while considering the sparse-checkout\n+    case.  I've been tripped up by it *many* times.\n+  * I think trying to reuse the existing parsing engine and have it handling\n+    both old and new syntax is a recipe for failure.  It'd be much cleaner\n+    to have errors thrown if the processing turns up any \"precious\" files,\n+    or perhaps if any line starts with '$'.\n+  * I think making a copy of the existing parsing, and then letting them\n+    diverge, means the two will eventually diverge even further, and we\n+    would need to make a copy of all the documentation about gitignore rules\n+    for sparse-checkout, all for the non-default non-cone case we are\n+    already recommending users away from.\n+\n+Behavior of traditional flags\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+There are two flags to consider here: the --porcelain flag to git-status,\n+and the --no-overwrite-ignore command to checkout & merge commands.  For\n+the --porcelain flag to git-status, see the \"Breakdown of suggested\n+behaviors by command\" and look for git-status there.  The rest of this\n+section will focus on --[no-]overwrite-ignore.\n+\n+People have wanted precious files long enough, that they implemented an\n+interim kludge of sorts -- a command line option that can be passed to\n+various subcommands that treats all ignored files as precious:\n+--no-overwrite-ignore.\n+\n+In particular, this flag can be passed to both git-checkout, and git-merge.\n+However, in merge's case, the support depended the flag being passed to the\n+backend and the backend supporting it.  The builtin/merge.c code only ever\n+bothered to pass this flag down to the fast-forwarding merge handling code,\n+so it never worked with any backends that actually create a merge commit.\n+\n+We do need to keep these flags working, at least as much as they did\n+previously.  However, we don't want to consider them desired features,\n+which would lead us to making related equivalents for precious files like\n+--overwrite-precious.  Instead we will:\n+  * Keep --[no-]overwrite-ignore working, as much as it already was.\n+  * Recommend users mark precious files in their gitignore files instead of\n+    using these flags\n+\n+Interaction with older Git clients\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+Older Git clients will not understand precious files.  This means that:\n+  * precious files will be considered untracked and not ignored.\n+  * most comands will preserve these files, since untracked-and-not-ignored\n+    are not considered expendable.\n+  * git status will continue listing these files\n+  * git add will add these files without requiring -f.\n+\n+This seems like a reasonable tradeoff that only has minor annoyances.  The\n+alternative of having the precious files treated as ignored has the very\n+risky trade-off of deleting files which the users marked as important for\n+us to keep.\n+\n+Commands with modified meaning\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+In clean, we adjust the meaning of both -x and -X:\n+  -X: remove only trashable files\n+  -x: remove untracked and trashable files (but preserve precious ones)\n+\n+Implementation hints\n+--------------------\n+\n+Data structures\n+~~~~~~~~~~~~~~~\n+  * We will want to add a `precious` and `precious_nr` in dir_struct,\n+    similar to the current entries/nr or ignored/ignored_nr.\n+  * We may want to rename `ignored` and `ignored_nr` in dir_struct to\n+    `trashable` and `trashable_nr`.\n+\n+Code areas\n+~~~~~~~~~~\n+  * \"preserve_ignored\", a flag in the code for handling the\n+    --[no-]overwrite-ignore flag, is a very helpful marker about what needs\n+    to be tweaked and how to tweak it to preserve more files.  In particular,\n+    note that --no-overwrite-ignore works by telling the machinery in dir.c\n+    to not do the setup_standard_excludes() stuff, so that all ignored files\n+    just look like untracked files.  We'll need something slightly smarter,\n+    which makes precious files look like untracked while trashable files\n+    still appear in ignored.  Shouldn't be too bad.\n+  * we might need to add another entry to the unpack_trees_reset_type\n+    enum.  Or perhaps we still keep both UNPACK_RESET_PROTECT_UNTRACKED\n+    and UNPACK_RESET_OVERWRITE_UNTRACKED but rename them with\n+    s/UNTRACKED/NOT_EXPENDABLE/ so it is clear they handle both untracked and\n+    precious files.  Not sure which is needed yet.\n+  * dir_struct->flags _might_ need new entries.\n+  * ensure all relevant codepaths touched by 94b7f1563ac (\"Comment important\n+    codepaths regarding nuking untracked files/dirs\", 2021-09-27) are either\n+    fixed or also mention precious files\n+  * am/rebase/checkout[without -f]: see 480d3d6bf90 (\"Change unpack_trees'\n+    'reset' flag into an enum\", 2021-09-27)\n+  * Merge backends:\n+    * (see also \"Out of scope\" section)\n+    * merge-ort can be fixed by fixing the checkout code.\n+    * merge-resolve and merge-octopus can probably be fixed by fixing\n+      git-reset.\n+  * stash:\n+    * there is an existing --include-untracked option.  There was no reason\n+      to add a --include-ignored, because ignored files were trashable.  Do\n+      we need to add a --include-precious, though?\n+    * this is a sad pile of shell-reimplemented-in-C.  It's just awful.\n+      See b34ab4a43ba (\"stash: remove unnecessary process forking\",\n+      2020-12-01) and ba359fd5070 (\"stash: fix stash application in\n+      sparse-checkouts\", 2020-12-01) and 94b7f1563ac (\"Comment important\n+      codepaths regarding nuking untracked files/dirs\", 2021-09-27).\n+      Fixing stash to not nuke precious files (and to not nuke untracked\n+      files either) might mean expunging the stupid\n+      shell-reimplemented-in-C design, or at least moving things more in\n+      that direction.\n+  * rebase (merge backend), revert, cherry-pick, am -3: should automatically\n+    be handled by getting merge-ort to work, which should work by making\n+    checkout/switch work.\n+  * bisect: should work by making checkout work\n+\n+Minimum\n+~~~~~~~\n+\n+I think for a minimum implementation, we need to ensure that the following\n+are handled:\n+  * parsing:\n+    * parsing of lines starting with '$' in .gitignore\n+    * erroring on lines starting with '!$' in .gitignore\n+    * erroring on lines starting with '$' in $GIT_DIR/info/sparse-checkout\n+  * commands with support:\n+    * switch/checkout\n+    * merge when using the ort backend\n+    * read-tree -u [without --reset] (due to internal use)\n+    * ls-files\n+\n+Out of scope\n+------------\n+The following tasks are currently out of scope for this proposal:\n+\n+apply, am [without -3]: apply won't overwrite any file in the working\n+  directory even when a new file is in the patch.  It should overwrite\n+  trashable files.  We could log that bug via testcase, but make sure\n+  there's a companion testcase that ensures overwriting untracked or\n+  precious files continues to make apply throw an error.  However, since\n+  apply/am don't misbehave for precious files, we can defer this to later.\n+\n+checkout-index: similar to apply; won't overwrite any existing files, but\n+  trashable files should be overwritten\n+\n+reset --hard:\n+  * `git reset --hard` is a little funny and we have thought about changing\n+    it[4].  However, that can be left for later and will not be tackled as\n+    part of the work of introducing \"precious\" files as a concept.\n+\n+merge backends:\n+  * it may make sense to try to make --no-overwrite-ignore work with more\n+    merge backends, both because it's technically documented behavior, and\n+    because doing so may be a step towards getting precious files supported\n+  * when multiple merge strategies are specified, builtin/merge.c will\n+    stash and restore state between the attempt of different strategies.\n+    Since the reset_hard() function invokes `read-tree --reset -u`, there\n+    might be a way to cause it to trash untracked files or to trash\n+    precious files, depending on what the merge strategies did.  It seems\n+    unlikely (maybe the strategy handles D/F conflicts or rename\n+    conflicts by renaming files in the way, and happens to rename a\n+    precious file to a path that is considered either untracked or\n+    precious -- merge-recursive certainly did this something like this\n+    once upon a time and still might); we can probably ignore it for now.\n+  * merge-recursive is a lost cause; it'd be a _huge_ amount of effort to\n+    fix, but we intend to deprecate and delete it soon anyway (making all\n+    requests for recursive just trigger ort instead).\n+  * user-defined merge strategies are up to their authors to get right.\n+    Odds are they won't, but odds are they already incorrectly nuke\n+    untracked files too because who'd pay attention to a special case\n+    like files being in the way of a merge?  Anyway, \"not our problem\".  :-)\n+\n+Previous discussions\n+--------------------\n+\n+A far from exhaustive sampling of various past conversations on the topic:\n+\n+[P1] https://lore.kernel.org/git/7vipsnar23.fsf@alter.siamese.dyndns.org/\n+[P2] https://lore.kernel.org/git/xmqqttqytnqb.fsf@gitster.g/\n+[P3] https://lore.kernel.org/git/79901E6C-9839-4AB2-9360-9EBCA1AAE549@icloud.com/\n+[P4] https://lore.kernel.org/git/87a6q9kacx.fsf@evledraar.gmail.com/\n+[P5] https://lore.kernel.org/git/20190216114938.18843-1-pclouds@gmail.com/\n+[P6] https://lore.kernel.org/git/87ftsi68ke.fsf@evledraar.gmail.com/\n+[P7] https://lore.kernel.org/git/xmqqo7ub4sfh.fsf@gitster.g/\n+[P8] https://lore.kernel.org/git/7v4oepaup7.fsf@alter.siamese.dyndns.org/\n+[P9] https://lore.kernel.org/git/20181112232209.GK890086@genre.crustytoothpaste.net/\n+[P10] https://lore.kernel.org/git/xmqqttqvg4lw.fsf@gitster.g/\n+[P11] https://lore.kernel.org/git/xmqqk1hrr91s.fsf@gitster-ct.c.googlers.com/\n+[P12] https://lore.kernel.org/git/9C4A2AFD-AAA2-4ABA-8A8B-2133FD870366@icloud.com/\n+[P13] https://lore.kernel.org/git/xmqqfs2e3292.fsf@gitster.g/\n+[P14] https://lore.kernel.org/git/0deee2bc-1775-4459-906d-1d44b3103499@gmail.com/\n+[P15] https://lore.kernel.org/git/ZSkpOc%2FdcGcrFQNU@ugly/\n+[P16] https://lore.kernel.org/git/xmqqil79t82q.fsf@gitster.g/\n+[P17] https://lore.kernel.org/git/xmqqo7h6tnib.fsf@gitster.g/\n+\n+Alternatives considered\n+-----------------------\n+There have been multiple alternatives considered, along a few different\n+axes:\n+  * .gitattributes instead of .gitignore\n+  * leaving sparse-checkout alone\n+  * Trashable [P9,P11]\n+  * Alternative gitignore syntax\n+\n+The choice of .gitattributes vs .gitignore was already addressed in the\n+\"Precious file specification\" section.\n+\n+The choice to modify or leave alone the parsing of\n+$GIT_DIR/info/sparse-checkout was already addressed in the \"Interaction\n+with sparse-checkout parsing\" section.\n+\n+One alternative raised in the past was treating ignored files as not\n+expendable by default, and then introducing a new category of\n+ignored-but-expendable.  This new category has been dubbed \"trashable\" in\n+the past.  That may have been a reasonable solution if Git did not have a\n+large userbase already, but moving in this direction would cause severe\n+problems for existing builds everywhere[P9] and would require users to\n+doubly configure most files (since it is expected that\n+ignored-but-expendable is a much larger class of files than\n+ignored-but-precious).  See also [P11].\n+\n+There have been multiple alternative suggestions for extending gitignore\n+syntax to handle precious files and optionally future extensions as well.\n+For example: [P10, P12, P13, P14, P15, P16]  However:\n+  * There have been on and off requests for precious files for about 14\n+    years\n+  * We are not aware of other types of extensions needed; there might\n+    not be any\n+  * The alternatives all seem much more complex to explain to users than\n+    the simple proposal here.\n+In particular, we like the simplicity of the providing the simple mapping\n+to users from the penultimate paragraph of the \"Precious file\n+specification\" section (the one regarding no-prefix vs. '!' vs '$').\n-- \n2.56.0\n\n\n"},{"id":"554489","messageId":"adbfbf5aab70bdf8cdb2ac080ed79a3144edcb8d.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"[RFC PATCH 2/6] dir.h: replace pattern macros with enum in attr.h","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:06:58Z","receivedAt":"2026-10-08T12:06:58Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"dir.h defines some macros that are used in the dir.c machinery for\n.gitignore files. These macros are only used for the pattern struct\ndefined in attr.h so it really should belong there.\n\nRemove the macro definitions from dir.h and replace them with the\npattern_flags enum in attr.h.\n\nSigned-off-by: Mirko Faina <mroik@delayed.space>\n---\n attr.h                    | 9 ++++++++-\n builtin/check-ignore.c    | 1 +\n builtin/sparse-checkout.c | 1 +\n dir.c                     | 5 +++--\n dir.h                     | 8 ++------\n 5 files changed, 15 insertions(+), 9 deletions(-)\n\ndiff --git a/attr.h b/attr.h\nindex a04a521092..c083d47df5 100644\n--- a/attr.h\n+++ b/attr.h\n@@ -250,11 +250,18 @@ struct attr_state {\n \tconst char *setto;\n };\n \n+enum pattern_flags {\n+\tPATTERN_FLAG_NODIR = 1,\n+\tPATTERN_FLAG_ENDSWITH = 4,\n+\tPATTERN_FLAG_MUSTBEDIR = 8,\n+\tPATTERN_FLAG_NEGATIVE = 16,\n+};\n+\n struct pattern {\n \tconst char *pattern;\n \tint patternlen;\n \tint nowildcardlen;\n-\tunsigned flags;\t\t/* PATTERN_FLAG_* */\n+\tenum pattern_flags flags;\t/* PATTERN_FLAG_* */\n };\n \n /*\ndiff --git a/builtin/check-ignore.c b/builtin/check-ignore.c\nindex 644c9a414f..fdf7610178 100644\n--- a/builtin/check-ignore.c\n+++ b/builtin/check-ignore.c\n@@ -9,6 +9,7 @@\n #include \"parse-options.h\"\n #include \"submodule.h\"\n #include \"write-or-die.h\"\n+#include \"attr.h\"\n \n static int quiet, verbose, stdin_paths, show_non_matching, no_index;\n static const char * const check_ignore_usage[] = {\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex cb4a037b77..83981e2b1e 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -20,6 +20,7 @@\n #include \"setup.h\"\n #include \"sparse-index.h\"\n #include \"worktree.h\"\n+#include \"attr.h\"\n \n static const char *empty_base = \"\";\n \ndiff --git a/dir.c b/dir.c\nindex d896e7be4b..c6342c882a 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -36,6 +36,7 @@\n #include \"trace2.h\"\n #include \"tree.h\"\n #include \"hex.h\"\n+#include \"attr.h\"\n \n  /*\n   * The maximum size of a pattern/exclude file. If the file exceeds this size\n@@ -701,7 +702,7 @@ int no_wildcard(const char *string)\n \n void parse_path_pattern(const char **pattern,\n \t\t\t   int *patternlen,\n-\t\t\t   unsigned *flags,\n+\t\t\t   enum pattern_flags *flags,\n \t\t\t   int *nowildcardlen)\n {\n \tconst char *p = *pattern;\n@@ -979,7 +980,7 @@ void add_pattern(const char *string, const char *base,\n {\n \tstruct path_pattern *pattern;\n \tint patternlen;\n-\tunsigned flags;\n+\tenum pattern_flags flags;\n \tint nowildcardlen;\n \n \tparse_path_pattern(&string, &patternlen, &flags, &nowildcardlen);\ndiff --git a/dir.h b/dir.h\nindex 83e0f648a8..210ee8a98d 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -6,6 +6,7 @@\n #include \"pathspec.h\"\n #include \"statinfo.h\"\n #include \"strbuf.h\"\n+#include \"attr.h\"\n \n struct repository;\n \n@@ -49,11 +50,6 @@ struct dir_entry {\n \tchar name[FLEX_ARRAY]; /* more */\n };\n \n-#define PATTERN_FLAG_NODIR 1\n-#define PATTERN_FLAG_ENDSWITH 4\n-#define PATTERN_FLAG_MUSTBEDIR 8\n-#define PATTERN_FLAG_NEGATIVE 16\n-\n struct path_pattern {\n \t/*\n \t * This allows callers of last_matching_pattern() etc.\n@@ -469,7 +465,7 @@ int add_patterns_from_blob_to_list(struct object_id *oid,\n int add_patterns_from_buffer(char *buf, size_t size,\n \t\t\t     const char *base, int baselen,\n \t\t\t     struct pattern_list *pl);\n-void parse_path_pattern(const char **string, int *patternlen, unsigned *flags, int *nowildcardlen);\n+void parse_path_pattern(const char **string, int *patternlen, enum pattern_flags *flags, int *nowildcardlen);\n void add_pattern(const char *string, const char *base,\n \t\t int baselen, struct pattern_list *pl, int srcpos);\n void clear_pattern_list(struct pattern_list *pl);\n-- \n2.56.0\n\n\n"},{"id":"554490","messageId":"677a1af4c6bacf129450e3c0c150d27bf65baecf.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"[RFC PATCH 3/6] dir.c: teach parse_path_pattern() precious files","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:06:59Z","receivedAt":"2026-10-08T12:06:59Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"parse_path_pattern() knows only to recognize patterns that are either\ndirectories or normal files. It also knows that there are negated\npatterns, but it doesn't know about patterns that indicate\nprecious files.\n\nTeach about precious files to parse_path_pattern(). We teach it also to\ndetect the invalid patterns starting with '!$' which would mean a\nnegated precious file, that we don't allow. This is reported back to the\ncaller so we change its signature to return an int. We also teach the\ncallers parse_attr_line() and add_pattern() to handle such errors.\n\nSigned-off-by: Mirko Faina <mroik@delayed.space>\n---\n attr.c            |  8 ++++++--\n attr.h            |  1 +\n dir.c             | 26 +++++++++++++++++++++++---\n dir.h             |  2 +-\n t/t7508-status.sh |  9 +++++++++\n 5 files changed, 40 insertions(+), 6 deletions(-)\n\ndiff --git a/attr.c b/attr.c\nindex 0e63f1b6de..5c60ca6b23 100644\n--- a/attr.c\n+++ b/attr.c\n@@ -382,10 +382,14 @@ struct match_attr *parse_attr_line(const char *line, const char *src,\n \t\tchar *p = (char *)&(res->state[num_attr]);\n \t\tmemcpy(p, name, namelen);\n \t\tres->u.pat.pattern = p;\n-\t\tparse_path_pattern(&res->u.pat.pattern,\n+\t\tif (parse_path_pattern(&res->u.pat.pattern,\n \t\t\t\t      &res->u.pat.patternlen,\n \t\t\t\t      &res->u.pat.flags,\n-\t\t\t\t      &res->u.pat.nowildcardlen);\n+\t\t\t\t      &res->u.pat.nowildcardlen)) {\n+\t\t\twarning(_(\"pattern '%s' is invalid, skipping\"),\n+\t\t\t\t  (char *)&res->u.pat.pattern);\n+\t\t\tgoto fail_return;\n+\t\t}\n \t\tif (res->u.pat.flags & PATTERN_FLAG_NEGATIVE) {\n \t\t\twarning(_(\"Negative patterns are ignored in git attributes\\n\"\n \t\t\t\t  \"Use '\\\\!' for literal leading exclamation.\"));\ndiff --git a/attr.h b/attr.h\nindex c083d47df5..d00939732b 100644\n--- a/attr.h\n+++ b/attr.h\n@@ -255,6 +255,7 @@ enum pattern_flags {\n \tPATTERN_FLAG_ENDSWITH = 4,\n \tPATTERN_FLAG_MUSTBEDIR = 8,\n \tPATTERN_FLAG_NEGATIVE = 16,\n+\tPATTERN_FLAG_PRECIOUS = 32,\n };\n \n struct pattern {\ndiff --git a/dir.c b/dir.c\nindex c6342c882a..c6f1bed429 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -700,7 +700,10 @@ int no_wildcard(const char *string)\n \treturn string[simple_length(string)] == '\\0';\n }\n \n-void parse_path_pattern(const char **pattern,\n+/*\n+ * Returns 1 if the pattern is problematic, 0 otherwise\n+ */\n+int parse_path_pattern(const char **pattern,\n \t\t\t   int *patternlen,\n \t\t\t   enum pattern_flags *flags,\n \t\t\t   int *nowildcardlen)\n@@ -709,7 +712,12 @@ void parse_path_pattern(const char **pattern,\n \tsize_t i, len;\n \n \t*flags = 0;\n-\tif (*p == '!') {\n+\tif (simple_length(p) >= 2 && p[0] == '!' && p[1] == '$') {\n+\t\treturn 1;\n+\t} else if (*p == '$') {\n+\t\t*flags |= PATTERN_FLAG_PRECIOUS;\n+\t\tp++;\n+\t} else if (*p == '!') {\n \t\t*flags |= PATTERN_FLAG_NEGATIVE;\n \t\tp++;\n \t}\n@@ -736,6 +744,7 @@ void parse_path_pattern(const char **pattern,\n \t\t*flags |= PATTERN_FLAG_ENDSWITH;\n \t*pattern = p;\n \t*patternlen = len;\n+\treturn 0;\n }\n \n int pl_hashmap_cmp(const void *cmp_data UNUSED,\n@@ -975,6 +984,14 @@ int hashmap_contains_parent(struct hashmap *map,\n \treturn 0;\n }\n \n+/*\n+ * Parses the pattern for its type and sets flags accordingly, then adds it to\n+ * the pattern list. If the pattern is invalid the function returns early with a\n+ * warning.\n+ *\n+ * The only problematic patterns at the moment are the one starting with '!$'\n+ * which are negated precious-files.\n+ */\n void add_pattern(const char *string, const char *base,\n \t\t int baselen, struct pattern_list *pl, int srcpos)\n {\n@@ -983,7 +1000,10 @@ void add_pattern(const char *string, const char *base,\n \tenum pattern_flags flags;\n \tint nowildcardlen;\n \n-\tparse_path_pattern(&string, &patternlen, &flags, &nowildcardlen);\n+\tif (parse_path_pattern(&string, &patternlen, &flags, &nowildcardlen)) {\n+\t\twarning(_(\"pattern '%s' is problematic, skipping\"), string);\n+\t\treturn;\n+\t}\n \tFLEX_ALLOC_MEM(pattern, pattern, string, patternlen);\n \tpattern->patternlen = patternlen;\n \tpattern->nowildcardlen = nowildcardlen;\ndiff --git a/dir.h b/dir.h\nindex 210ee8a98d..5cda2cdba7 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -465,7 +465,7 @@ int add_patterns_from_blob_to_list(struct object_id *oid,\n int add_patterns_from_buffer(char *buf, size_t size,\n \t\t\t     const char *base, int baselen,\n \t\t\t     struct pattern_list *pl);\n-void parse_path_pattern(const char **string, int *patternlen, enum pattern_flags *flags, int *nowildcardlen);\n+int parse_path_pattern(const char **string, int *patternlen, enum pattern_flags *flags, int *nowildcardlen);\n void add_pattern(const char *string, const char *base,\n \t\t int baselen, struct pattern_list *pl, int srcpos);\n void clear_pattern_list(struct pattern_list *pl);\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex 0fd7c79911..aa251c6abd 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -301,6 +301,15 @@ EOF\n \ttest_cmp expect output\n '\n \n+test_expect_success 'parse invalid pattern' \"\n+\ttest_when_finished rm actual gitignore_backup &&\n+\ttest_when_finished cp gitignore_backup .gitignore &&\n+\tcp .gitignore gitignore_backup &&\n+\techo '!$/ciao' >> .gitignore &&\n+\tgit status >actual 2>&1 &&\n+\ttest_grep \\\"warning: pattern '!$/ciao' is problematic, skipping\\\" actual\n+\"\n+\n test_expect_success 'status with gitignore (nothing untracked)' '\n \t{\n \t\techo \".gitignore\" &&\n-- \n2.56.0\n\n\n"},{"id":"554491","messageId":"b4e2d50f6f5da34a9720d23f487f0db41d2bf3ee.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"[RFC PATCH 4/6] dir.c: teach add_pattern() reject precious pattern","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:07:00Z","receivedAt":"2026-10-08T12:07:00Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"There are some places where we want to disallow precious files patterns\naltogether. This is the case with $GIT_DIR/info/sparse-checkout.\n\nTeach add_pattern() a flag to indicate if we want it to reject\nprecious files patterns. If that's the case it fails the same way it\nwould when we encounter a pattern that starts with '!$', with a warning.\n\nSigned-off-by: Mirko Faina <mroik@delayed.space>\n---\n builtin/clean.c                    |  4 ++--\n builtin/ls-files.c                 |  2 +-\n builtin/sparse-checkout.c          | 15 +++++++++------\n dir.c                              | 20 +++++++++++++++-----\n dir.h                              |  5 +++--\n t/helper/test-path-walk.c          |  2 +-\n t/t1091-sparse-checkout-builtin.sh |  8 ++++++++\n 7 files changed, 39 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/clean.c b/builtin/clean.c\nindex 1d5e7e5366..73408da5c2 100644\n--- a/builtin/clean.c\n+++ b/builtin/clean.c\n@@ -707,7 +707,7 @@ static int filter_by_patterns_cmd(void)\n \t\t\titem = &ignore_list.items[i];\n \t\t\tif (!*item->string)\n \t\t\t\tcontinue;\n-\t\t\tadd_pattern(item->string, \"\", 0, pl, -(i+1));\n+\t\t\tadd_pattern(item->string, \"\", 0, pl, -(i+1), 0);\n \t\t}\n \n \t\tchanged = 0;\n@@ -1018,7 +1018,7 @@ int cmd_clean(int argc,\n \n \tpl = add_pattern_list(&dir, EXC_CMDL, \"--exclude option\");\n \tfor (i = 0; i < exclude_list.nr; i++)\n-\t\tadd_pattern(exclude_list.items[i].string, \"\", 0, pl, -(i+1));\n+\t\tadd_pattern(exclude_list.items[i].string, \"\", 0, pl, -(i+1), 0);\n \n \tparse_pathspec(&pathspec, 0,\n \t\t       PATHSPEC_PREFER_CWD,\ndiff --git a/builtin/ls-files.c b/builtin/ls-files.c\nindex b044520f9e..d1cc7e92f4 100644\n--- a/builtin/ls-files.c\n+++ b/builtin/ls-files.c\n@@ -685,7 +685,7 @@ int cmd_ls_files(int argc,\n \t\t\tls_files_usage, 0);\n \tpl = add_pattern_list(&dir, EXC_CMDL, \"--exclude option\");\n \tfor (i = 0; i < exclude_list.nr; i++) {\n-\t\tadd_pattern(exclude_list.items[i].string, \"\", 0, pl, --exclude_args);\n+\t\tadd_pattern(exclude_list.items[i].string, \"\", 0, pl, --exclude_args, 0);\n \t}\n \n \tif (format && (show_stage || show_others || show_killed ||\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex 83981e2b1e..615007148e 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -455,6 +455,8 @@ static struct sparse_checkout_init_opts {\n \tint sparse_index;\n } init_opts;\n \n+#define PATTERN_DISALLOW_PRECIOUS (1<<1)\n+\n static int sparse_checkout_init(int argc, const char **argv, const char *prefix,\n \t\t\t\tstruct repository *repo)\n {\n@@ -487,7 +489,8 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix,\n \tmemset(&pl, 0, sizeof(pl));\n \n \tsparse_filename = get_sparse_checkout_filename();\n-\tres = add_patterns_from_file_to_list(sparse_filename, \"\", 0, &pl, NULL, 0);\n+\tres = add_patterns_from_file_to_list(sparse_filename, \"\", 0, &pl, NULL,\n+\t\t\t\t\t     PATTERN_DISALLOW_PRECIOUS);\n \n \t/* If we already have a sparse-checkout file, use it. */\n \tif (res >= 0) {\n@@ -515,8 +518,8 @@ static int sparse_checkout_init(int argc, const char **argv, const char *prefix,\n \n \tfree(sparse_filename);\n \n-\tadd_pattern(\"/*\", empty_base, 0, &pl, 0);\n-\tadd_pattern(\"!/*/\", empty_base, 0, &pl, 0);\n+\tadd_pattern(\"/*\", empty_base, 0, &pl, 0, 0);\n+\tadd_pattern(\"!/*/\", empty_base, 0, &pl, 0, 0);\n \tpl.use_cone_patterns = init_opts.cone_mode;\n \n \treturn write_patterns_and_update(repo, &pl);\n@@ -618,12 +621,12 @@ static void add_patterns_from_input(struct pattern_list *pl,\n \t\t\tstruct strbuf line = STRBUF_INIT;\n \n \t\t\twhile (!strbuf_getline(&line, file))\n-\t\t\t\tadd_pattern(line.buf, empty_base, 0, pl, 0);\n+\t\t\t\tadd_pattern(line.buf, empty_base, 0, pl, 0, 0);\n \n \t\t\tstrbuf_release(&line);\n \t\t} else {\n \t\t\tfor (i = 0; i < argc; i++)\n-\t\t\t\tadd_pattern(argv[i], empty_base, 0, pl, 0);\n+\t\t\t\tadd_pattern(argv[i], empty_base, 0, pl, 0, 0);\n \t\t}\n \t}\n }\n@@ -1079,7 +1082,7 @@ static int sparse_checkout_disable(int argc, const char **argv,\n \tpl.use_cone_patterns = 0;\n \tcfg->apply_sparse_checkout = 1;\n \n-\tadd_pattern(\"/*\", empty_base, 0, &pl, 0);\n+\tadd_pattern(\"/*\", empty_base, 0, &pl, 0, 0);\n \n \tprepare_repo_settings(the_repository);\n \trepo->settings.sparse_index = 0;\ndiff --git a/dir.c b/dir.c\nindex c6f1bed429..9aba1716a6 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -993,7 +993,8 @@ int hashmap_contains_parent(struct hashmap *map,\n  * which are negated precious-files.\n  */\n void add_pattern(const char *string, const char *base,\n-\t\t int baselen, struct pattern_list *pl, int srcpos)\n+\t\t int baselen, struct pattern_list *pl, int srcpos,\n+\t\t int disable_precious)\n {\n \tstruct path_pattern *pattern;\n \tint patternlen;\n@@ -1004,6 +1005,12 @@ void add_pattern(const char *string, const char *base,\n \t\twarning(_(\"pattern '%s' is problematic, skipping\"), string);\n \t\treturn;\n \t}\n+\n+\tif (disable_precious && (flags & PATTERN_FLAG_PRECIOUS)) {\n+\t\twarning(_(\"'$%s' precious-files pattern not allowed here, skipping\"),\n+\t\t\tstring);\n+\t\treturn;\n+\t}\n \tFLEX_ALLOC_MEM(pattern, pattern, string, patternlen);\n \tpattern->patternlen = patternlen;\n \tpattern->nowildcardlen = nowildcardlen;\n@@ -1162,6 +1169,7 @@ static void invalidate_directory(struct untracked_cache *uc,\n \n /* Flags for add_patterns() */\n #define PATTERN_NOFOLLOW (1<<0)\n+#define PATTERN_DISALLOW_PRECIOUS (1<<1)\n \n /*\n  * Given a file with name \"fname\", read it (either from disk, or from\n@@ -1244,14 +1252,15 @@ static int add_patterns(const char *fname, const char *base, int baselen,\n \t\treturn -1;\n \t}\n \n-\tadd_patterns_from_buffer(buf, size, base, baselen, pl);\n+\tadd_patterns_from_buffer(buf, size, base, baselen, pl,\n+\t\t\t\t flags & PATTERN_DISALLOW_PRECIOUS);\n \tfree(buf);\n \treturn 0;\n }\n \n int add_patterns_from_buffer(char *buf, size_t size,\n \t\t\t     const char *base, int baselen,\n-\t\t\t     struct pattern_list *pl)\n+\t\t\t     struct pattern_list *pl, int disable_precious)\n {\n \tchar *orig = buf;\n \tint i, lineno = 1;\n@@ -1270,7 +1279,8 @@ int add_patterns_from_buffer(char *buf, size_t size,\n \t\t\tif (entry != buf + i && entry[0] != '#') {\n \t\t\t\tbuf[i - (i && buf[i-1] == '\\r')] = 0;\n \t\t\t\ttrim_trailing_spaces(entry);\n-\t\t\t\tadd_pattern(entry, base, baselen, pl, lineno);\n+\t\t\t\tadd_pattern(entry, base, baselen, pl, lineno,\n+\t\t\t\t\t    disable_precious);\n \t\t\t}\n \t\t\tlineno++;\n \t\t\tentry = buf + i + 1;\n@@ -1307,7 +1317,7 @@ int add_patterns_from_blob_to_list(\n \t\treturn -1;\n \t}\n \n-\tadd_patterns_from_buffer(buf, size, base, baselen, pl);\n+\tadd_patterns_from_buffer(buf, size, base, baselen, pl, 0);\n \tfree(buf);\n \treturn 0;\n }\ndiff --git a/dir.h b/dir.h\nindex 5cda2cdba7..a6977149b8 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -464,10 +464,11 @@ int add_patterns_from_blob_to_list(struct object_id *oid,\n \t\t\t\t   struct pattern_list *pl);\n int add_patterns_from_buffer(char *buf, size_t size,\n \t\t\t     const char *base, int baselen,\n-\t\t\t     struct pattern_list *pl);\n+\t\t\t     struct pattern_list *pl, int disable_precious);\n int parse_path_pattern(const char **string, int *patternlen, enum pattern_flags *flags, int *nowildcardlen);\n void add_pattern(const char *string, const char *base,\n-\t\t int baselen, struct pattern_list *pl, int srcpos);\n+\t\t int baselen, struct pattern_list *pl, int srcpos,\n+\t\t int disable_precious);\n void clear_pattern_list(struct pattern_list *pl);\n void dir_clear(struct dir_struct *dir);\n \ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 4233badb58..fefe885ec6 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -124,7 +124,7 @@ int cmd__path_walk(int argc, const char **argv)\n \t\tinfo.pl->use_cone_patterns = 1;\n \n \t\tstrbuf_fread(&in, 2048, stdin);\n-\t\tadd_patterns_from_buffer(in.buf, in.len, \"\", 0, info.pl);\n+\t\tadd_patterns_from_buffer(in.buf, in.len, \"\", 0, info.pl, 0);\n \t\tstrbuf_release(&in);\n \t}\n \ndiff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh\nindex 74b1761e0c..caae112037 100755\n--- a/t/t1091-sparse-checkout-builtin.sh\n+++ b/t/t1091-sparse-checkout-builtin.sh\n@@ -252,6 +252,14 @@ test_expect_success 'sparse-checkout disable' '\n \tcheck_files repo a deep folder1 folder2\n '\n \n+test_expect_success 'skip precious-file pattern in $GIT_DIR/info/sparse-checkout' \"\n+\ttest_when_finished rm actual .git/info/sparse-checkout &&\n+\ttest_when_finished git sparse-checkout disable &&\n+\techo \\\"$/ciao\\\" > .git/info/sparse-checkout &&\n+\tgit sparse-checkout init >actual 2>&1 &&\n+\ttest_grep \\\"warning: '$/ciao' precious-files pattern not allowed here, skipping\\\" actual\n+\"\n+\n test_expect_success 'sparse-index enabled and disabled' '\n \tgit -C repo sparse-checkout init --cone --sparse-index &&\n \ttest_cmp_config -C repo true index.sparse &&\n-- \n2.56.0\n\n\n"},{"id":"554492","messageId":"ae61067e7b835a6dd65f9047a19bb93f3f82f746.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"[RFC PATCH 5/6] unpack-trees: teach check_ok_to_remove() precious","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:07:01Z","receivedAt":"2026-10-08T12:07:01Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"\"git switch\" and \"git checkout\" only clobber local changes when these\npaths are marked as excluded. With the introduction of precious files we\nwant to make sure that the underlying machinery understands the\ndifference between a trashable and a precious file (which are both\nexcluded/ignored).\n\nTeach check_ok_to_remove() to only target trashable files for clobbering\ninstead of targeting excluded files.\n\nSigned-off-by: Mirko Faina <mroik@delayed.space>\n---\n  This is missing some tests for clobbering, but I wasn't sure where to place\n  them.\n\n dir.c          | 19 ++++++++++++++++++-\n dir.h          |  4 ++++\n unpack-trees.c |  7 ++++---\n 3 files changed, 26 insertions(+), 4 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 9aba1716a6..ffc1818533 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -1861,7 +1861,24 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir,\n \n /*\n  * Loads the exclude lists for the directory containing pathname, then\n- * scans all exclude lists to determine whether pathname is excluded.\n+ * scans all exclude lists to determine whether pathname is trashable.\n+ * Returns 1 if true, otherwise 0.\n+ */\n+int is_trashable(struct dir_struct *dir, struct index_state *istate,\n+\t\tconst char *pathname, int *dtype_p)\n+{\n+\tstruct path_pattern *pattern =\n+\t\tlast_matching_pattern(dir, istate, pathname, dtype_p);\n+\tif (pattern)\n+\t\treturn pattern->flags &\n+\t\t       (PATTERN_FLAG_NEGATIVE | PATTERN_FLAG_PRECIOUS) ? 0 : 1;\n+\treturn 0;\n+}\n+\n+/*\n+ * Loads the exclude lists for the directory containing pathname, then\n+ * scans all exclude lists to determine whether pathname is excluded (both\n+ * trashable and precious).\n  * Returns 1 if true, otherwise 0.\n  */\n int is_excluded(struct dir_struct *dir, struct index_state *istate,\ndiff --git a/dir.h b/dir.h\nindex a6977149b8..5424036e4e 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -442,6 +442,10 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir,\n \t\t\t\t\t   struct index_state *istate,\n \t\t\t\t\t   const char *name, int *dtype);\n \n+int is_trashable(struct dir_struct *dir,\n+\t\tstruct index_state *istate,\n+\t\tconst char *name, int *dtype);\n+\n int is_excluded(struct dir_struct *dir,\n \t\tstruct index_state *istate,\n \t\tconst char *name, int *dtype);\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex 1802809ad3..632fefd260 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -2434,11 +2434,12 @@ static int check_ok_to_remove(const char *name, int len, int dtype,\n \tif (repo_ignore_case(the_repository) && icase_exists(o, name, len, st))\n \t\treturn 0;\n \n+\t/* Check if trashable */\n \tif (o->internal.dir &&\n-\t    is_excluded(o->internal.dir, o->src_index, name, &dtype))\n+\t    is_trashable(o->internal.dir, o->src_index, name, &dtype))\n \t\t/*\n-\t\t * ce->name is explicitly excluded, so it is Ok to\n-\t\t * overwrite it.\n+\t\t * ce->name is explicitly marked as trashable,\n+\t\t * so it is Ok to overwrite it.\n \t\t */\n \t\treturn 0;\n \tif (S_ISDIR(st->st_mode)) {\n-- \n2.56.0\n\n\n"},{"id":"554493","messageId":"f5abaac0ff3b1d4598fa27bbcf338e1a36a08098.1791460418.git.mroik@delayed.space","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"[RFC PATCH 6/6] builtin/ls-files.c: support for precious files","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:07:02Z","receivedAt":"2026-10-08T12:07:02Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"Teach \"git ls-files\" new variants of \"-i\" with --ignored=trashable\nand --ignored=precious, where git shows only trashable files with the\nfirst and only precious files with the second. If no optional argument\nis passed \"ls-files\" retains the current behaviour.\n\nSigned-off-by: Mirko Faina <mroik@delayed.space>\n---\n builtin/ls-files.c                 | 73 ++++++++++++++++++++++++++---\n dir.c                              | 74 ++++++++++++++++++++++++++----\n dir.h                              | 34 +++++++++++++-\n t/t2205-add-worktree-config.sh     |  2 +-\n t/t3001-ls-files-others-exclude.sh | 27 +++++++++++\n 5 files changed, 190 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/ls-files.c b/builtin/ls-files.c\nindex d1cc7e92f4..63dd769385 100644\n--- a/builtin/ls-files.c\n+++ b/builtin/ls-files.c\n@@ -163,8 +163,21 @@ static void show_dir_entry(struct index_state *istate,\n \twrite_name(ent->name);\n }\n \n-static void show_other_files(struct index_state *istate,\n-\t\t\t     const struct dir_struct *dir)\n+static void show_precious_files(struct index_state *istate,\n+\t\t\t\tconst struct dir_struct *dir)\n+{\n+\tint i;\n+\n+\tfor (i = 0; i < dir->precious_nr; i++) {\n+\t\tstruct dir_entry *ent = dir->precious[i];\n+\t\tif (!index_name_is_other(istate, ent->name, ent->len))\n+\t\t\tcontinue;\n+\t\tshow_dir_entry(istate, tag_other, ent);\n+\t}\n+}\n+\n+static void show_entries_files(struct index_state *istate,\n+\t\t\t       const struct dir_struct *dir)\n {\n \tint i;\n \n@@ -176,6 +189,24 @@ static void show_other_files(struct index_state *istate,\n \t}\n }\n \n+static void show_ignored_files(struct index_state *istate,\n+\t\t\t       const struct dir_struct *dir)\n+{\n+\tshow_entries_files(istate, dir);\n+\tshow_precious_files(istate, dir);\n+}\n+\n+static void show_other_files(struct index_state *istate,\n+\t\t\t     const struct dir_struct *dir)\n+{\n+\tif (dir->flags & DIR_SHOW_IGNORED)\n+\t\tshow_ignored_files(istate, dir);\n+\telse if (dir->flags & DIR_SHOW_PRECIOUS)\n+\t\tshow_precious_files(istate, dir);\n+\telse\n+\t\tshow_entries_files(istate, dir);\n+}\n+\n static void show_killed_files(struct index_state *istate,\n \t\t\t      const struct dir_struct *dir)\n {\n@@ -547,6 +578,30 @@ static const char * const ls_files_usage[] = {\n \tNULL\n };\n \n+static int option_parse_ignored(const struct option *opt, const char *arg, int unset)\n+{\n+\tenum dir_struct_flags *flags = opt->value;\n+\n+\tBUG_ON_OPT_NEG(unset);\n+\n+\t/*\n+\t * Reset in case --ignored=<type> is specified multiple times, we keep\n+\t * the last one.\n+\t */\n+\t*flags &= ~(DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS);\n+\n+\tif (!arg)\n+\t\t*flags |= DIR_SHOW_IGNORED;\n+\telse if (!strcmp(arg, \"trashable\"))\n+\t\t*flags |= DIR_SHOW_TRASHABLE;\n+\telse if (!strcmp(arg, \"precious\"))\n+\t\t*flags |= DIR_SHOW_PRECIOUS;\n+\telse\n+\t\tdie(_(\"'%s' argument is not a valid value\"), arg);\n+\n+\treturn 0;\n+}\n+\n static int option_parse_exclude(const struct option *opt,\n \t\t\t\tconst char *arg, int unset)\n {\n@@ -615,9 +670,9 @@ int cmd_ls_files(int argc,\n \t\t\tN_(\"show modified files in the output\")),\n \t\tOPT_BOOL('o', \"others\", &show_others,\n \t\t\tN_(\"show other files in the output\")),\n-\t\tOPT_BIT('i', \"ignored\", &dir.flags,\n+\t\tOPT_CALLBACK_F('i', \"ignored\", &dir.flags, N_(\"type\"),\n \t\t\tN_(\"show ignored files in the output\"),\n-\t\t\tDIR_SHOW_IGNORED),\n+\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NONEG, option_parse_ignored),\n \t\tOPT_BOOL('s', \"stage\", &show_stage,\n \t\t\tN_(\"show staged contents' object name in the output\")),\n \t\tOPT_BOOL('k', \"killed\", &show_killed,\n@@ -704,7 +759,9 @@ int cmd_ls_files(int argc,\n \t\ttag_skip_worktree = \"S \";\n \t\ttag_resolve_undo = \"U \";\n \t}\n-\tif (show_modified || show_others || show_deleted || (dir.flags & DIR_SHOW_IGNORED) || show_killed)\n+\tif (show_modified || show_others || show_deleted ||\n+\t    (dir.flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) ||\n+\t    show_killed)\n \t\trequire_work_tree = 1;\n \tif (show_unmerged)\n \t\t/*\n@@ -753,10 +810,12 @@ int cmd_ls_files(int argc,\n \tif (pathspec.nr && error_unmatch)\n \t\tps_matched = xcalloc(pathspec.nr, 1);\n \n-\tif ((dir.flags & DIR_SHOW_IGNORED) && !show_others && !show_cached)\n+\tif ((dir.flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) &&\n+\t    !show_others && !show_cached)\n \t\tdie(\"ls-files -i must be used with either -o or -c\");\n \n-\tif ((dir.flags & DIR_SHOW_IGNORED) && !exc_given)\n+\tif ((dir.flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) &&\n+\t    !exc_given)\n \t\tdie(\"ls-files --ignored needs some exclude pattern\");\n \n \t/* With no flags, we default to showing the cached files */\ndiff --git a/dir.c b/dir.c\nindex ffc1818533..274a934e12 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -281,10 +281,25 @@ int fill_directory(struct dir_struct *dir,\n {\n \tconst char *matched_prefix;\n \tsize_t prefix_len;\n+\tunsigned exclusive;\n+\tint i;\n \n-\tunsigned exclusive_flags = DIR_SHOW_IGNORED | DIR_SHOW_IGNORED_TOO;\n-\tif ((dir->flags & exclusive_flags) == exclusive_flags)\n-\t\tBUG(\"DIR_SHOW_IGNORED and DIR_SHOW_IGNORED_TOO are exclusive\");\n+\tconst unsigned exclusive_with_too[] = {\n+\t\tDIR_SHOW_IGNORED,\n+\t\tDIR_SHOW_TRASHABLE,\n+\t\tDIR_SHOW_PRECIOUS,\n+\t};\n+\tconst char *exclusive_bug_text[] = {\n+\t\t\"DIR_SHOW_IGNORED\",\n+\t\t\"DIR_SHOW_TRASHABLE\",\n+\t\t\"DIR_SHOW_PRECIOUS\",\n+\t};\n+\tfor (i = 0; i < 3; i++) {\n+\t\texclusive = (exclusive_with_too[i] | DIR_SHOW_IGNORED_TOO);\n+\t\tif ((dir->flags & exclusive) == exclusive)\n+\t\t\tBUG(\"%s and DIR_SHOW_IGNORED_TOO are exclusive\",\n+\t\t\t    exclusive_bug_text[i]);\n+\t}\n \n \t/*\n \t * Calculate common prefix for the pathspec, and\n@@ -1859,6 +1874,21 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir,\n \t\t\tbasename, dtype_p);\n }\n \n+/*\n+ * Loads the exclude lists for the directory containing pathname, then\n+ * scans all exclude lists to determine whether pathname is precious.\n+ * Returns 1 if true, otherwise 0.\n+ */\n+int is_precious(struct dir_struct *dir, struct index_state *istate,\n+\t\tconst char *pathname, int *dtype_p)\n+{\n+\tstruct path_pattern *pattern =\n+\t\tlast_matching_pattern(dir, istate, pathname, dtype_p);\n+\tif (pattern)\n+\t\treturn !!(pattern->flags & PATTERN_FLAG_PRECIOUS);\n+\treturn 0;\n+}\n+\n /*\n  * Loads the exclude lists for the directory containing pathname, then\n  * scans all exclude lists to determine whether pathname is trashable.\n@@ -1911,6 +1941,17 @@ static struct dir_entry *dir_add_name(struct dir_struct *dir,\n \treturn dir->entries[dir->nr++] = dir_entry_new(pathname, len);\n }\n \n+static struct dir_entry *dir_add_precious(struct dir_struct *dir,\n+\t\t\t\t      struct index_state *istate,\n+\t\t\t\t      const char *pathname, int len)\n+{\n+\tif (index_file_exists(istate, pathname, len, repo_ignore_case(the_repository)))\n+\t\treturn NULL;\n+\n+\tALLOC_GROW(dir->precious, dir->precious_nr+1, dir->internal.precious_alloc);\n+\treturn dir->precious[dir->precious_nr++] = dir_entry_new(pathname, len);\n+}\n+\n struct dir_entry *dir_add_ignored(struct dir_struct *dir,\n \t\t\t\t  struct index_state *istate,\n \t\t\t\t  const char *pathname, int len)\n@@ -2528,13 +2569,18 @@ static enum path_treatment treat_path(struct dir_struct *dir,\n \t    (directory_exists_in_index(istate, path->buf, path->len) == index_nonexistent))\n \t\treturn path_none;\n \n-\texcluded = is_excluded(dir, istate, path->buf, &dtype);\n+\texcluded = is_excluded(dir, istate, path->buf, &dtype) ||\n+\t\t   is_trashable(dir, istate, path->buf, &dtype) ||\n+\t\t   is_precious(dir, istate, path->buf, &dtype);\n \n \t/*\n \t * Excluded? If we don't explicitly want to show\n \t * ignored files, ignore it\n \t */\n-\tif (excluded && !(dir->flags & (DIR_SHOW_IGNORED|DIR_SHOW_IGNORED_TOO)))\n+\tif (excluded && !(dir->flags & (DIR_SHOW_IGNORED|\n+\t\t\t\t\tDIR_SHOW_TRASHABLE|\n+\t\t\t\t\tDIR_SHOW_PRECIOUS|\n+\t\t\t\t\tDIR_SHOW_IGNORED_TOO)))\n \t\treturn path_excluded;\n \n \tswitch (dtype) {\n@@ -2704,20 +2750,28 @@ static void add_path_to_appropriate_result_list(struct dir_struct *dir,\n \tconst struct pathspec *pathspec,\n \tenum path_treatment state)\n {\n+\tint dtype;\n+\n \t/* add the path to the appropriate result list */\n \tswitch (state) {\n \tcase path_excluded:\n-\t\tif (dir->flags & DIR_SHOW_IGNORED)\n-\t\t\tdir_add_name(dir, istate, path->buf, path->len);\n-\t\telse if ((dir->flags & DIR_SHOW_IGNORED_TOO) ||\n+\t\tif (dir->flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS)) {\n+\t\t\tdtype = resolve_dtype(cdir->d_type, istate, path->buf, path->len);\n+\t\t\tif (is_precious(dir, istate, path->buf, &dtype))\n+\t\t\t\tdir_add_precious(dir, istate, path->buf, path->len);\n+\t\t\telse\n+\t\t\t\tdir_add_name(dir, istate, path->buf, path->len);\n+\t\t} else if ((dir->flags & DIR_SHOW_IGNORED_TOO) ||\n \t\t\t((dir->flags & DIR_COLLECT_IGNORED) &&\n \t\t\texclude_matches_pathspec(path->buf, path->len,\n-\t\t\t\t\t\t pathspec)))\n+\t\t\t\t\t\t pathspec))) {\n \t\t\tdir_add_ignored(dir, istate, path->buf, path->len);\n+\t\t}\n \t\tbreak;\n \n \tcase path_untracked:\n-\t\tif (dir->flags & DIR_SHOW_IGNORED)\n+\t\tif (dir->flags & (DIR_SHOW_IGNORED | DIR_SHOW_TRASHABLE |\n+\t\t\t\t  DIR_SHOW_PRECIOUS))\n \t\t\tbreak;\n \t\tdir_add_name(dir, istate, path->buf, path->len);\n \t\tif (cdir->fdir)\ndiff --git a/dir.h b/dir.h\nindex 5424036e4e..58e5467cda 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -212,14 +212,30 @@ struct untracked_cache {\n struct dir_struct {\n \n \t/* bit-field of options */\n-\tenum {\n+\tenum dir_struct_flags {\n \n \t\t/**\n \t\t * Return just ignored files in `entries[]`, not untracked files.\n \t\t * This flag is mutually exclusive with `DIR_SHOW_IGNORED_TOO`.\n+\t\t * This is a superset of DIR_SHOW_TRASHABLE and DIR_SHOW_PRECIOUS.\n+\t\t *\n+\t\t * This flag should probably be dropped throughout the codebase\n+\t\t * in favour of (DIR_SHOW_TRASHABLE | DIR_SHOW_PRECIOUS).\n \t\t */\n \t\tDIR_SHOW_IGNORED = 1<<0,\n \n+\t\t/**\n+\t\t * Return just trashable files in `entries[]`, not untracked files.\n+\t\t * This flag is mutually exclusive with `DIR_SHOW_IGNORED_TOO`.\n+\t\t */\n+\t\tDIR_SHOW_TRASHABLE = 1<<10,\n+\n+\t\t/**\n+\t\t * Return just precious files in `entries[]`, not untracked files.\n+\t\t * This flag is mutually exclusive with `DIR_SHOW_IGNORED_TOO`.\n+\t\t */\n+\t\tDIR_SHOW_PRECIOUS = 1<<11,\n+\n \t\t/* Include a directory that is not tracked. */\n \t\tDIR_SHOW_OTHER_DIRECTORIES = 1<<1,\n \n@@ -243,7 +259,8 @@ struct dir_struct {\n \t\t/**\n \t\t * Similar to `DIR_SHOW_IGNORED`, but return ignored files in\n \t\t * `ignored[]` in addition to untracked files in `entries[]`.\n-\t\t * This flag is mutually exclusive with `DIR_SHOW_IGNORED`.\n+\t\t * This flag is mutually exclusive with `DIR_SHOW_IGNORED`,\n+\t\t * `DIR_SHOW_TRASHABLE` and `DIR_SHOW_PRECIOUS`.\n \t\t */\n \t\tDIR_SHOW_IGNORED_TOO = 1<<5,\n \n@@ -278,12 +295,18 @@ struct dir_struct {\n \t/* The number of members in `entries[]` array. */\n \tint nr; /* output only */\n \n+\t/* The number of members in `precious[]` array. */\n+\tint precious_nr; /* output only */\n+\n \t/* The number of members in `ignored[]` array. */\n \tint ignored_nr; /* output only */\n \n \t/* An array of `struct dir_entry`, each element of which describes a path. */\n \tstruct dir_entry **entries; /* output only */\n \n+\t/* Used for listing precious files with `DIR_SHOW_PRECIOUS`. */\n+\tstruct dir_entry **precious; /* output only */\n+\n \t/**\n \t * used for ignored paths with the `DIR_SHOW_IGNORED_TOO` and\n \t * `DIR_COLLECT_IGNORED` flags.\n@@ -307,6 +330,9 @@ struct dir_struct {\n \t\t/* Keeps track of allocation of `entries[]` array.*/\n \t\tint alloc;\n \n+\t\t/* Keeps track of allocation of `precious[]` array.*/\n+\t\tint precious_alloc;\n+\n \t\t/* Keeps track of allocation of `ignored[]` array. */\n \t\tint ignored_alloc;\n \n@@ -442,6 +468,10 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir,\n \t\t\t\t\t   struct index_state *istate,\n \t\t\t\t\t   const char *name, int *dtype);\n \n+int is_precious(struct dir_struct *dir,\n+\t\tstruct index_state *istate,\n+\t\tconst char *name, int *dtype);\n+\n int is_trashable(struct dir_struct *dir,\n \t\tstruct index_state *istate,\n \t\tconst char *name, int *dtype);\ndiff --git a/t/t2205-add-worktree-config.sh b/t/t2205-add-worktree-config.sh\nindex 43d950de64..a743470196 100755\n--- a/t/t2205-add-worktree-config.sh\n+++ b/t/t2205-add-worktree-config.sh\n@@ -244,7 +244,7 @@ test_expect_success '3a: setup--add repo dir' '\n test_expect_success '3b: ignored' '\n \t(\n \tcd test3 &&\n-\tgit --git-dir=repo/.git ls-files -io --directory --exclude-standard >actual-ignored-unsorted &&\n+\tgit --git-dir=repo/.git ls-files -i -o --directory --exclude-standard >actual-ignored-unsorted &&\n \tsort actual-ignored-unsorted >actual-ignored &&\n \tsort expect-ignored-unsorted >expect-ignored &&\n \ttest_cmp expect-ignored actual-ignored\ndiff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh\nindex 29a0a25b30..977cbd6b6e 100755\n--- a/t/t3001-ls-files-others-exclude.sh\n+++ b/t/t3001-ls-files-others-exclude.sh\n@@ -55,6 +55,7 @@ expect\n !*.8' >.git/ignore\n \n echo '*.1\n+$wasder\n /*.3\n !*.6' >.gitignore\n echo '*.2\n@@ -76,6 +77,32 @@ test_expect_success 'git ls-files --others with various exclude options.' '\n \ttest_cmp expect output\n '\n \n+test_expect_success 'git ls-files -o -i' '\n+\ttouch wasder &&\n+\tgit ls-files -o -i --exclude-standard >output &&\n+\tcat output &&\n+\ttest_grep \"wasder\" output\n+'\n+\n+test_expect_success 'git ls-files -o --ignored=trashable' '\n+\tgit ls-files -o --ignored=trashable --exclude-standard >output &&\n+\tcat output &&\n+\ttest_grep ! \"wasder\" output\n+'\n+\n+test_expect_success 'git ls-files -o --ignored=precious' '\n+\tgit ls-files -o --ignored=precious --exclude-standard >output &&\n+\tcat output &&\n+\ttest_grep \"wasder\" output\n+'\n+\n+test_expect_success 'git ls-files -o --ignored=trashable' '\n+\ttest_when_finished rm wasder &&\n+\tgit ls-files -o --ignored=precious --ignored=trashable --exclude-standard >output &&\n+\tcat output &&\n+\ttest_grep ! \"wasder\" output\n+'\n+\n # Test \\r\\n (MSDOS-like systems)\n printf '*.1\\r\\n/*.3\\r\\n!*.6\\r\\n' >.gitignore\n \n-- \n2.56.0\n\n\n"},{"id":"554494","messageId":"aseIX3hABbm822X8@exploit","threadId":"66490","inReplyTo":"cover.1791460418.git.mroik@delayed.space","subject":"Re: [RFC PATCH 0/6] Introduce precious files","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-10-08T12:12:47Z","receivedAt":"2026-10-08T12:12:47Z","isPatch":true,"sender":{"key":"mroik@delayed.space","avatar":"https://avatars.githubusercontent.com/u/25752903?v=4"},"body":"On Thu, Oct 08, 2026 at 02:06:56PM +0200, Mirko Faina wrote:\n> Introduce precious files based on Elijah Newren's document [1].\n> \n> With this series documentation is not updated yet but I wanted some feedback\n> before polishing and moving on to implement the changes for \"git clean\".\n> \n> [1] https://lore.kernel.org/git/pull.1627.git.1703643931314.gitgitgadget@gmail.com/\n> \n> [1/6] precious-files.txt: new document proposing new precious file type (Elijah Newren)\n> [2/6] dir.h: replace pattern macros with enum in attr.h (Mirko Faina)\n> [3/6] dir.c: teach parse_path_pattern() precious files (Mirko Faina)\n> [4/6] dir.c: teach add_pattern() reject precious pattern (Mirko Faina)\n> [5/6] unpack-trees: teach check_ok_to_remove() precious (Mirko Faina)\n> [6/6] builtin/ls-files.c: support for precious files (Mirko Faina)\n> \n>  Documentation/technical/precious-files.txt | 540 +++++++++++++++++++++\n>  attr.c                                     |   8 +-\n>  attr.h                                     |  10 +-\n>  builtin/check-ignore.c                     |   1 +\n>  builtin/clean.c                            |   4 +-\n>  builtin/ls-files.c                         |  75 ++-\n>  builtin/sparse-checkout.c                  |  16 +-\n>  dir.c                                      | 144 +++++-\n>  dir.h                                      |  51 +-\n>  t/helper/test-path-walk.c                  |   2 +-\n>  t/t1091-sparse-checkout-builtin.sh         |   8 +\n>  t/t2205-add-worktree-config.sh             |   2 +-\n>  t/t3001-ls-files-others-exclude.sh         |  27 ++\n>  t/t7508-status.sh                          |   9 +\n>  unpack-trees.c                             |   7 +-\n>  15 files changed, 849 insertions(+), 55 deletions(-)\n>  create mode 100644 Documentation/technical/precious-files.txt\n> \n> -- \n> 2.56.0\n\nI forgot to specify the base. The series is based on c46c1e3772 (Start Git 2.98\ncycle, 2026-09-30).\n\n"}]}