Skip to content

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.

--changed filters per-file rules to the diff while cross-file and existence rules stay whole-tree A repository of four files has two changed. Per-file rules receive only the two changed files. Cross-file and existence rules receive all four files, so a whole-tree invariant is still checked. repository 2 of 4 files changed src/parser.rschanged docs/guide.mdchanged src/lib.rsunchanged api.hunchanged per-file rules see the changed set src/parser.rs docs/guide.md 2 of 4 evaluated no_trailing_whitespace, file_header, filename_case cross-file + existence see the whole tree src/parser.rs docs/guide.md src/lib.rs api.h pair, file_exists cross-file and existence rules stay whole-tree for correctness

--changed picks its diff from git in one of two shapes:

InvocationDiff sourceFits
alint check --changedstaged, unstaged, and untracked paths (--exclude-standard)pre-commit, local dev
alint check --changed --base=mainmain...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.

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). A pair rule that requires every api.h to have an api.c must still fire when you delete api.c, even though the surviving api.h is 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 their paths: scope does not intersect the diff, so a missing LICENSE fails only the PRs that touch a LICENSE-shaped path, and adding Readme.md next to a committed README.md is 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 a json_schema_passes rule 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_together asks whether a file's partner changed alongside it, and changeset_requires_path gates on what a commit range touched, so they read the changed set by design. markdown_paths_resolve checks every in-scope document under --changed (like markdown_links_resolve), because deleting or renaming a referenced file breaks a document you did not touch.
  • 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 git missing from PATH): --changed hard-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 LICENSE you deleted is in the changed set, the walker no longer sees it on disk, and an existence rule for LICENSE evaluates 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.

A PR check that lints only the files the branch touched, against the merge-base:

Terminal window
alint check --changed --base=origin/main

If 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 whitespace
error header-source-pair api.h has no matching api.c

The 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.

  • 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.