Skip to content

midsbie/grint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

grint, a Git repository linter and hygiene tool

Overview

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.

Installation

Requires Python >= 3.11.

python -m venv .venv
. .venv/bin/activate
make setup

Dependencies (installed automatically): pygit2, pydantic, pyyaml, click, anyio.

Quick Start

  1. Create a default config (in your OS app config dir) and review it:
grint config init
grint config validate
  1. Run a scan over the current directory:
grint scan .
  1. JSON output:
grint scan . --format json
  1. List available plugins (discovered via entry points):
grint list-plugins

Configuration

grint 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: []

Per-Policy Visibility (Narrowing)

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: { }

How It Works

  • Discovery: recursively finds repos by .git directories.
  • 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 Finding records with id, severity, message, data, suggest_actions.
  • Output: human-friendly with color by default, or JSON per finding.

Core Plugins

All plugin configuration lives under each policy’s config key alongside optional name and visibility.

remotes

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: true

dirty_worktree

Thresholds 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: false

unpushed_commits

Warns when ahead of upstream beyond tolerance.

version: 1
policies:
  - plugin: unpushed_commits
    config:
      visibility: { include: [], exclude: [] }
      max_ahead: 0

unpulled_commits

Warns when behind upstream beyond tolerance.

version: 1
policies:
  - plugin: unpulled_commits
    config:
      visibility: { include: [], exclude: [] }
      max_behind: 0

diverged_branch

Error when both ahead > 0 and behind > 0.

version: 1
policies:
  - plugin: diverged_branch
    config:
      visibility: { include: [], exclude: [] }
      severity: error

unmerged_conflicts

Error on merge/rebase in progress or conflicted files.

version: 1
policies:
  - plugin: unmerged_conflicts
    config:
      visibility: { include: [], exclude: [] }
      severity: error

detached_head

Warning 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: error

default_branch_policy

Enforce 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: warning

JSON Output

Each 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"]}

Development

  • Code style: idiomatic Python 3.11; plugins pure over RepoContext.
  • Run tests:
pytest -q

License

Released under the MIT license; refer to LICENSE for more details.

About

A Git repository linter and hygiene tool

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors