Changed mode
alint check --changed layers a diff filter on top of the walk so a per-file rule sees only the files you touched, which is what a pre-commit hook or a PR check wants. Most cross-file rules, and the existence rules, deliberately opt out and keep evaluating the whole tree, because an invariant they enforce can be broken by a file you changed even when its partner did not.
The two diff modes
Section titled "The two diff modes"--changed picks its diff from git in one of two shapes:
| Invocation | Diff source | Fits |
|---|---|---|
alint check --changed | staged, unstaged, and untracked paths (--exclude-standard) | pre-commit, local dev |
alint check --changed --base=main | main...HEAD (three-dot, merge-base) | PR checks |
The three-dot <base>...HEAD form diffs against the merge-base of <base> and HEAD, which is exactly what a GitHub PR calls "your changes." --base=<ref> implies --changed, so the ref is the verb's argument; you never pass both --changed and a bare --base awkwardly. The same flags work on alint fix --changed.
Which rules stay whole-tree
Section titled "Which rules stay whole-tree"The filter narrows which files per-file rules check, but a checked file is always judged against the whole repository: whether it is in scope (a scope_filter.has_ancestor marker such as an unchanged Cargo.toml still counts) and whether the paths it references exist both look at the full tree. Several families also opt out of the filter, on purpose:
- Whole-tree rules always evaluate against the whole tree, because their verdict depends on files outside your diff: the relational rules (
pair,for_each_dir,for_each_file,every_matching_has,unique_by,dir_contains,dir_only_contains), the manifest and graph rules (cross_file,file_graph,registry_paths_resolve,pair_hash), and the single-shot rules (generated_file_fresh,command_idempotent). Apairrule that requires everyapi.hto have anapi.cmust still fire when you deleteapi.c, even though the survivingapi.his not itself in your diff. - Existence and tree-shape rules also consult the whole tree, but the two file-existence rules (
file_exists,file_absent) and the two tree-shape rules (no_case_conflicts,max_files_per_directory) are skipped when theirpaths:scope does not intersect the diff, so a missingLICENSEfails only the PRs that touch aLICENSE-shaped path, and addingReadme.mdnext to a committedREADME.mdis caught. When they do run they judge the whole tree, so they can also report a standing violation in an unchanged file. The two directory-existence rules (dir_exists,dir_absent) always evaluate, since a directory scope never intersects a file-path diff. - Rules that depend on a file outside their scope run when that file changes: editing only the
schema_path:of ajson_schema_passesrule re-validates every document it covers. - Diff-oriented rules are the mirror image: a few cross-file rules exist because of the diff.
pair_changed_togetherasks whether a file's partner changed alongside it, andchangeset_requires_pathgates on what a commit range touched, so they read the changed set by design.markdown_paths_resolvechecks every in-scope document under--changed(likemarkdown_links_resolve), because deleting or renaming a referenced file breaks a document you did not touch.
Edge cases
Section titled "Edge cases"- Empty diff (nothing staged, modified, or untracked): the run short-circuits to an empty report in milliseconds unless an active rule has
expect_matches: true. Those scope assertions still inspect the full index so configuration drift cannot hide behind an incremental run; the underlying rules remain skipped. - Outside a git repo (or
gitmissing fromPATH):--changedhard-errors rather than silently falling back to a full check, because a silent full run would betray the intent the flag expressed. - Deleted files appear in the diff. A
LICENSEyou deleted is in the changed set, the walker no longer sees it on disk, and an existence rule forLICENSEevaluates the whole tree (which now lacks it) and fires. - Partly staged files are checked from the working tree, because that is what alint reads. The pre-commit framework temporarily stashes unstaged hunks, so its working tree matches the index while the hook runs; a hand-written hook should do the same if it needs index-exact content.
--changed pairs naturally with git_tracked_only:: the changed set covers the index and working tree, while the tracked set records what is in the index, so a rule with both fires only on tracked entries that are part of this diff.
In practice
Section titled "In practice"A PR check that lints only the files the branch touched, against the merge-base:
alint check --changed --base=origin/mainIf the branch added a trailing-whitespace line to src/parser.rs and deleted api.c (leaving api.h orphaned), one report carries both a per-file finding and a whole-tree one:
warning no-trailing-whitespace trailing whitespaceerror header-source-pair api.h has no matching api.cThe no-trailing-whitespace finding came through the diff filter; the pair finding came from the whole-tree pass, even though api.h was never in the diff.
Going deeper
Section titled "Going deeper"- The walker and git is the whole-tree index this filter sits on top of.
- Scoping covers
changed_since:, the per-rule scope_filter counterpart to the run-wide--changed. - Configuration documents the rule kinds and their cross-file versus per-file classification.