grint helps you keep many local Git repositories clean, consistent, and in sync with policy. It scans directories for repos, runs a set of configurable plugins, and produces actionable findings with safe suggestions.
Key traits:
- Git-only, fast and deterministic.
- Policies in YAML; no provider names hardcoded.
- Plugins are pure and run in parallel.
- Per-policy visibility filtering to target subsets of repos.
Requires Python >= 3.11.
python -m venv .venv
. .venv/bin/activate
make setupDependencies (installed automatically): pygit2, pydantic, pyyaml, click, anyio.
- Create a default config (in your OS app config dir) and review it:
grint config init
grint config validate- Run a scan over the current directory:
grint scan .- JSON output:
grint scan . --format json- List available plugins (discovered via entry points):
grint list-pluginsgrint loads a single YAML file and validates it. CLI flags override env vars override YAML (merging rules left to the loader or your shell tooling).
Minimal example:
version: 1
engine:
safety:
require_ff: true
allow_force_push: false
allow_mirror_push: false
scan:
include: []
exclude: ["vendor/*", "*/vendor/*", "third_party/*", "*/third_party/*"]
follow_worktrees: true
concurrency:
workers: 8
policies: []Each policy (plugin instance) may define visibility in its config. Visibility only narrows the set of repos selected by the global scan.
Rules:
- Global exclude always wins, for all plugins.
- Then per-plugin visibility.exclude wins over include.
- If visibility.include is empty, inherit all repos from global scan.
- Globs use
Path.match()semantics (*and?; no**).
Example (two instances of the same plugin targeting different repos):
version: 1
policies:
- plugin: remotes
config:
name: work-repos
visibility:
include: ["work/*", "clients/*"]
exclude: ["clients/legacy/*"]
remotes: { }
- plugin: remotes
config:
name: personal-repos
visibility:
include: ["personal/*"]
remotes: { }- Discovery: recursively finds repos by
.gitdirectories. - Backend: a per-repo backend is chosen once (pygit2 preferred; subprocess fallback) and exposed as
RepoContext.git. - Execution: enabled plugins run in parallel and emit
Findingrecords withid, severity, message, data, suggest_actions. - Output: human-friendly with color by default, or JSON per finding.
All plugin configuration lives under each policy’s config key alongside optional name and visibility.
Validate existence, naming, and URL patterns for Git remotes.
version: 1
policies:
- plugin: remotes
config:
visibility:
include: []
exclude: []
remotes:
github:
required: true
allowed_uri_patterns:
- "^git@github\\.com:org/.+\\.git$"
- "^https://github\\.com/org/.+\\.git$"
allowed_names: ["origin", "github"]
severity:
missing_role: error
owner_mismatch: error
name_nonconformant: warning
actions:
allow_rename: trueThresholds for unstaged, uncommitted, and optionally untracked files, with include/exclude globs.
version: 1
policies:
- plugin: dirty_worktree
config:
visibility: { include: [], exclude: ["vendor/*", "*/vendor/*"] }
thresholds:
max_unstaged: 0
max_uncommitted: 0
max_untracked: 0
filters:
include: []
exclude: ["vendor/*", "*/vendor/*"]
treat_untracked_as_dirty: falseWarns when ahead of upstream beyond tolerance.
version: 1
policies:
- plugin: unpushed_commits
config:
visibility: { include: [], exclude: [] }
max_ahead: 0Warns when behind upstream beyond tolerance.
version: 1
policies:
- plugin: unpulled_commits
config:
visibility: { include: [], exclude: [] }
max_behind: 0Error when both ahead > 0 and behind > 0.
version: 1
policies:
- plugin: diverged_branch
config:
visibility: { include: [], exclude: [] }
severity: errorError on merge/rebase in progress or conflicted files.
version: 1
policies:
- plugin: unmerged_conflicts
config:
visibility: { include: [], exclude: [] }
severity: errorWarning on detached HEAD; escalate to error if no local branch points to the current commit.
version: 1
policies:
- plugin: detached_head
config:
visibility: { include: [], exclude: [] }
warning_severity: warning
error_severity: errorEnforce allowed/disallowed default branch names and ensure presence of an allowed branch.
version: 1
policies:
- plugin: default_branch_policy
config:
visibility: { include: [], exclude: [] }
allowed_names: ["main"]
disallow_names: ["master"]
enforce_presence: true
severity:
noncompliant_current: warning
missing_allowed: warningEach finding is emitted as a JSON object in --format json:
{"id": "unpushed_commits.ahead_exceeded", "repo": "/path/repo", "severity": "warning", "message": "...", "data": {"ahead": 3, "behind": 0}, "suggest_actions": ["git push origin main"]}- Code style: idiomatic Python 3.11; plugins pure over
RepoContext. - Run tests:
pytest -qReleased under the MIT license; refer to LICENSE for more details.