Skip to content

The walker and git

Every run begins by walking your repository once into a sorted in-memory index, and every rule reads that index, never the raw filesystem and never git directly. What lands in the index is the working tree minus everything .gitignore excludes, so a rule’s idea of “what is here” is the un-ignored working tree. That is almost always what you want; the two places it diverges from git’s own index are the source of nearly every “why didn’t my rule fire” question.

The walker builds an index from the un-ignored tree; git_tracked_only reads git's index instead Files on disk split two ways. The default walked tree includes README.md and an un-ignored untracked file but drops a gitignored-but-committed file. The git index, which git_tracked_only consults, includes README.md and the committed file but not the untracked one. The two divergent files are the ones a mis-set gitignore hides. one walk, one index git_tracked_only reads git instead on disk README.mdcommitted config.local.ymlgitignored, committed scratch.tmpuntracked walk + .gitignore git ls-files walked tree default rule view README.md scratch.tmp config.local.yml the un-ignored tree, committed file filtered out git index git_tracked_only: true README.md config.local.yml scratch.tmp what git tracks, catches the drift the two files that differ are the ones a mis-set .gitignore hides

Starting at the path you pass to alint check (or the current directory), the walker yields every regular file under that root, except paths matched by any of: the repo’s .gitignore files (root, per-directory, and any in directories above the walk root), .git/info/exclude, your global gitignore (core.excludesFile), .ignore files (the same syntax, honored by the ignore crate that powers ripgrep and the walker), the .git/ directory itself, and anything in the config’s ignore: list.

Hidden files are included: alint walks .github/, .editorconfig, and .cargo/ by default. In-tree symlinks are followed, but a symlink whose target escapes the repo root, or that dangles, is pruned from the walk. No git repo is required, and the ignore rules do not need one either: a directory that is not a git repo still honors any .gitignore and .ignore files it contains, so filtering happens with or without a .git/. Only a directory with no ignore files at all shows literally every file.

Two config fields shape the filtering. respect_gitignore (default true) toggles every gitignore source at once; the CLI’s --no-gitignore forces it off for one run. ignore: adds gitignore-style patterns on top, and applies regardless of respect_gitignore, for exclusions that are an alint concern rather than a git one:

version: 1
ignore:
- "vendor/**"
- "**/*.snapshot.json"

Setting respect_gitignore: false is rarely useful during development: absence-style rules (dir_absent, file_absent) begin firing on every locally-built target/, node_modules/, and __pycache__/. It fits one-off audits of a build tree, or linting a directory that is not a git repo.

Because the walker filters by .gitignore, “the walked tree” is a close but imperfect stand-in for “what git would commit.” alint never reads .git/index or shells out to git ls-files for the walk, so the approximation drifts in two directions:

  • A gitignored-but-tracked file (added to git first and gitignored later, or forced in with git add -f) stays in git’s index on every commit, yet the walker filters it out. Absence rules never see it and content rules never inspect it.
  • An un-ignored untracked file (a scratch file no .gitignore pattern covers) is in the walked tree but not git’s index, so a rule fires on a file git is not tracking.

In a healthy repo neither case is common, and git ls-files <path> is the authoritative answer when you suspect one. When a rule must key off git’s index rather than the walked tree, set git_tracked_only: true on it. The rule then fires only for paths git actually tracks, independent of .gitignore state:

- id: target-not-tracked
kind: dir_absent
paths: "**/target"
git_tracked_only: true
level: error
target/ statedefaultgit_tracked_only
Gitignored, never builtsilentsilent
Gitignored, built locallysilentsilent
Not gitignored, on diskfiressilent (not in index)
Committed (not gitignored)firesfires
Gitignored but force-addedsilent (walker prunes it)fires
Not a git repofiressilent (no index)

git_tracked_only applies to the existence kinds file_exists, file_absent, dir_exists, and dir_absent; any other kind rejects it at load rather than ignoring it. Outside a git repo (or with git off PATH) the tracked set is empty, so absence rules with the flag become silent no-ops (there is nothing to commit) and existence rules with it fail conservatively (no file qualifies). The git-hygiene family also ships git_commit_message, git_no_denied_paths, and other git-aware kinds; see the rule reference for the full set.

The canonical “do not let target/ be committed” rule keys off the index, not the walked tree, so a locally-built target/ stays quiet while a force-added one is caught:

version: 1
rules:
- id: target-not-tracked
kind: dir_absent
paths: "**/target"
git_tracked_only: true
level: error
message: "target/ must not be committed"

On a repo where someone ran git add -f target/debug/build.log:

error target-not-tracked target/ must not be committed

The same rule is silent for every developer who merely built locally, because that target/ is gitignored and untracked.

  • Configuration is the field reference for ignore:, respect_gitignore, and every rule field.
  • Scoping is how a rule narrows within the index, with paths:, when:, and scope_filter:.
  • Changed mode restricts a run to the files in a diff, layered on top of the walk.
  • The interactive architecture diagrams include the walker view (walkerFlow) as an explorable model.