Skip to content

feat(config): discovery loader - Discover() (config phase B4) - #95

Merged
helly25 merged 1 commit into
mainfrom
feat/config-loader-discovery
Jun 23, 2026
Merged

feat(config): discovery loader - Discover() (config phase B4)#95
helly25 merged 1 commit into
mainfrom
feat/config-loader-discovery

Conversation

@helly25

@helly25 helly25 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Fourth slice of the config loader (#58), per docs/design-config.md.

What

xff/config/loader.{h,cc} - Discover(opts, read) locates and parses the layers into ConfigInputs (ready for ResolveConfig):

  • system /etc/xff.ini - always read; its [policy] is never skipped by --no-config.
  • user via UserConfigPath: $XFF_CONFIG, else $XDG_CONFIG_HOME/xff/config, else $HOME/.config/xff/config.
  • --xffrc=FILE armed into the user layer, in order (naming the file is the consent).

The FileReader is injected (an absl::FunctionRef), so discovery is unit-tested with an in-memory fake fs - no touching /etc or ~. run.cc will supply a std::ifstream-backed reader.

--no-config skips the user layer + explicit files (and ResolveConfig drops the system [defaults]), but the system file is still read so its policy reaches the phase-C gate.

Tests

UserConfigPath precedence + empty; system+user layering under an active --config; explicit --xffrc ordering; --no-config (policy still parsed, resolution empty); missing files -> empty layers. Green on default + --config=clang --config=asan; clang-format + no-em-dash clean.

Roadmap: B1, B2, B3, B4 (this) -> B5 run.cc wiring -> C policy gate -> D styles/features (#54) -> E argv[0]+cascade+explain (#59). Project cascade is phase E.

Fourth slice of the config loader (#58), per docs/design-config.md. xff/config/loader.{h,cc}: Discover(opts, read) locates and parses the layers into ConfigInputs:
- system /etc/xff.ini (always read; its [policy] is never skipped),
- user config via UserConfigPath ($XFF_CONFIG, else $XDG_CONFIG_HOME/xff/config, else $HOME/.config/xff/config),
- explicit --xffrc=FILE armed into the user layer, in order.

The FileReader is injected (run.cc will supply a std::ifstream-backed reader), so discovery is unit-tested without touching /etc or the home dir. --no-config skips the user layer and explicit files (ResolveConfig also drops the system [defaults]) but the system file is still read so its policy is available to the phase-C gate. Project cascade is phase E; no run.cc wiring yet (next slice).
@helly25
helly25 enabled auto-merge (squash) June 23, 2026 08:12
@helly25
helly25 merged commit 5f5132f into main Jun 23, 2026
5 checks passed
@helly25
helly25 deleted the feat/config-loader-discovery branch June 23, 2026 08:15
helly25 added a commit that referenced this pull request Jul 1, 2026
…, PR 4/4) (#201)

* Add -g / --gitignore: respect per-directory .gitignore (ignore family, PR 4/4)

Completes the core ignore family: -g / --gitignore makes xff respect a .gitignore
in every directory, reusing the #199 per-directory stack (deeper overrides
shallower). Off by default (find-compatible); --gitignore=off turns it back off, and
-u / --no-ignore remains the master switch over every source.

- run.cc: GitignoreEnabled(globals) (last occurrence wins, =off disables);
  ResolveIgnoreFileNames now builds the per-directory filename list from all sources,
  lowest precedence first: .gitignore (-g) < .ignore < .xffignore (--ignore-files),
  so within a directory the xff-specific file wins. -u/--no-ignore returns none.

Self-documentation (same change): --gitignore (alias -g) GlobalFlag in the Ignore /
filter group (flow into --help / --man / --markdown) + the kHelpText usage page.

cli/ignore_gitignore_test.sh: off by default, -g respects recursively, =off and
-u/--no-ignore disable, a nested .gitignore scopes to its subtree, --help topic.

Scope note: bare -g means ON today. The design's bare-g=auto (respect iff in a git
repo), .git/info/exclude, global core.excludesFile, and the -g+/-g- suffix-sign
spelling need git-repo detection + toggle-parser machinery not built yet; tracked as
a follow-up (#95). bazel test //... green (33 tests).

* ignore_gitignore_test: assert exit code + surface output on help-topic failure

The --help=-g assertion flaked once on the ubuntu clang-asan CI cell (intermittent,
not reproducible on macOS-asan / ubuntu-default / locally) as a bare 'substring
missing', hiding the xff subprocess's captured stderr. Assert the exit status and
print the captured output on mismatch so a recurrence shows the real cause (e.g. a
sanitizer abort) rather than masquerading. Diagnostic + stricter; no behavior change.
helly25 added a commit that referenced this pull request Jul 3, 2026
…) (#222)

New xff/repo module: FindRepoRoot walks up from a search root to the git
working-tree root (the first ancestor with a .git dir or file), probed via
the VFS (Stat), so a fake filesystem drives the unit tests.

Wire it into the -g / --gitignore ternary: bare -g / --gitignore now selects
AUTO (respect per-directory .gitignore only when a search root is inside a
git repo, matching git's own behavior), while --gitignore=on forces it on
regardless and --gitignore=off forces it off. Last occurrence wins; still
off by default (find-compatible), and -u / --no-ignore still overrides.
Previously bare -g meant unconditional on.

Self-doc: --gitignore GlobalFlag summary + kHelpText line describe the
ternary; design.md notes what shipped vs pending. Tests: repo_test (unit,
fake fs) + ignore_gitignore_test.sh (e2e: auto-on in a repo, auto-off
outside, =on forces outside, =off/-u disable).

Follow-ups (still #95): -g+/-g- short spellings, .git/info/exclude, and
core.excludesFile.
helly25 added a commit that referenced this pull request Jul 3, 2026
Complete the -g ternary's short forms: -g+ == --gitignore=on (force on),
-g- == --gitignore=off (force off), alongside the existing bare -g = auto.
Both are recognized in ResolveGitignoreMode (last occurrence wins); no
parser change is needed since leading dash tokens are already collected as
globals verbatim.

Self-doc: --gitignore display + summary and the kHelpText line show the
-g[+|-] forms; design.md moves them from pending to shipped. Tests:
ignore_gitignore_test.sh gains -g+ forces on outside a repo and -g- forces
off inside a repo (overriding the auto-on).

Remaining under #95: .git/info/exclude and core.excludesFile.
helly25 added a commit that referenced this pull request Jul 3, 2026
When gitignore is on and a search root is inside a git repo, rebase the
IgnoreFileCache ancestor walk on the REPO ROOT instead of the search root
(git/rg/fd behavior): every directory from the entry up to the repo root is
consulted for .gitignore -- including directories ABOVE the search root --
and the repo's .git/info/exclude is applied at the bottom, below every
.gitignore (matched relative to the repo root). Off a repo, or with
gitignore off, the walk stops at the search root as before, so existing
behavior (and every existing test) is unchanged; the new reach only appears
when running xff inside a repo subdirectory.

IgnoreFileCache gains a per-root Scope (repo root + the root's absolute form
for rebasing relative entries) and a RepoExcludeFor cache for
.git/info/exclude; the walk resolves entries to absolute in repo scope so
paths above the search root reconcile. AbsoluteDir moved above the class.

Tests: ignore_gitignore_test.sh gains .git/info/exclude honored, and -g from
a subdir honoring the repo-root .gitignore + .git/info/exclude above it.

Remaining under #95: global core.excludesFile.
helly25 added a commit that referenced this pull request Jul 3, 2026
Complete the .gitignore stack with git's global excludes, the lowest ignore
layer. xff/repo gains GlobalExcludesPath: reads core.excludesFile from the git
config files ($XDG_CONFIG_HOME/git/config then ~/.gitconfig, the latter
winning), expands a leading ~, and falls back to git's default
$XDG_CONFIG_HOME/git/ignore (~/.config/git/ignore) when unset. VFS-backed and
env-injected (GitConfigEnv), so it is unit-tested hermetically with a fake fs.

run.cc resolves the path once when gitignore is on (HOME/XDG_CONFIG_HOME from
the environment, read through walk_fs so --dry-run still consults it), loads it
into a PatternList, and hands it to IgnoreFileCache, which applies it in Decide
below .git/info/exclude (matched relative to the repo root). Empty (a no-op)
when gitignore is off or no file exists.

Tests: repo_test covers core.excludesFile parsing (case-insensitive section/key,
quotes, ~ expansion, XDG vs ~/.gitconfig precedence, default fallback, no-HOME).
ignore_gitignore_test.sh drives the real binary with a throwaway HOME for both
the default ~/.config/git/ignore and a core.excludesFile custom path.

This finishes #95's gitignore refinements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant