diffrag
AI-powered code reviewer for git repositories. Point it at two branches and get an LLM-generated review that understands the broader codebase through RAG (Retrieval-Augmented Generation).
Features
- AI code review — analyzes the diff between any two branches and reports on breaking changes, API surface changes, SOLID violations, code smells, security concerns, missing documentation, and more.
- RAG context — the full repository is indexed into a ChromaDB vector store so the LLM can reference relevant surrounding code, not just the changed lines.
- Smart index caching — the vector index is rebuilt only when the HEAD commit changes; repeated runs on the same commit reuse the cached index instantly.
- Interactive Q&A — open a REPL and ask questions about the codebase, with or without a diff; the index is built on demand and the session remembers prior exchanges so follow-up questions work naturally.
- Concurrent reviews — files are reviewed in parallel up to a configurable limit, cutting wall-clock time on large changesets; rate-limited requests are retried automatically with exponential backoff.
- Flexible AI backends — works with any OpenAI-compatible endpoint (OpenWebUI, Ollama
/v1, OpenAI) as well as Ollama's native API. - Fully configurable — settings can be set once in a user-level global config or overridden per-project; sane defaults work out of the box. Indexes are stored in the OS user cache directory — your project directories stay clean.
.gitignore-aware indexing — the repository's.gitignoreis parsed automatically and its patterns are respected during indexing; extra excludes can be added via config.
Installation
uv add diffrag # add to a project
uv tool install diffrag # install as a standalone CLI tool
Requires Python ≥ 3.12.
Setup
1. Create a config file
diffrag looks for configuration in the following order (first match wins):
--config <path>— explicit path passed on the command line.diffrag.toml— in the current working directory (project-level override)$(diffrag config-path)— user-level global config (set once, used everywhere)- Built-in defaults — everything works without any config file
Recommended: set up a global config once so every repository uses the same
model and endpoint without needing its own .diffrag.toml:
# Create the global config (creates the directory too)
diffrag create-config
# Open it in your editor
$EDITOR "$(diffrag config-path)"
For a project-specific override (e.g. a different base_branch or verbosity):
diffrag create-config --out .diffrag.toml
Per-project .diffrag.toml files take precedence over the global config.
Minimal config — Ollama for both LLM and embeddings:
[ai]
base_url = "http://localhost:11434"
model = "llama3.2"
[embedding]
base_url = "http://localhost:11434"
model = "nomic-embed-text"
OpenWebUI for the LLM, Ollama for embeddings:
[ai]
base_url = "http://localhost:3000"
model = "gpt-4o"
api_key = "your-openwebui-token"
[embedding]
base_url = "http://localhost:11434"
model = "nomic-embed-text"
The [ai] and [embedding] sections are independent — you can mix and match any OpenAI-compatible endpoint for each role.
CLI usage
review — generate a code review
# Review current branch (HEAD) against main
diffrag review --base main
# Review a specific branch against main
diffrag review --base main --head feature/my-feature
# Review against a tag or commit SHA
diffrag review --base v1.2.0 --head HEAD
# Review a repo at a different path
diffrag review --repo /path/to/repo --base main
# Use a non-default config file
diffrag review --base main --config /path/to/my.toml
Index caching: on the first run the repository is indexed and the result is
persisted to the OS user cache directory (run diffrag db-path to see the exact
path for a given repo). On subsequent runs the tool compares the current HEAD SHA
against the stored one — if they match the index is reused immediately; if they
differ (new commit or branch switch) the index is rebuilt automatically. Your
project directories are never modified.
ask — interactive Q&A about the codebase
# Ask about the codebase alone — no diff involved
diffrag ask
# Include the diff against main as additional context
diffrag ask --base main
diffrag ask --base main --head feature/my-feature
Opens an interactive prompt backed by the same RAG index. The index is built on
first use and refreshed automatically whenever HEAD has moved, so ask works on
a fresh clone without running review first. Type exit or press Ctrl-D to quit.
Without --base the questions are answered purely from repository context —
useful for exploring an unfamiliar codebase:
> How does the indexer decide when to rebuild?
> Where is the retry logic for rate-limited requests?
> Which module owns prompt construction?
With --base the diff is added to the context on top of the repository
index, which suits review follow-ups:
> What does the change in auth.py affect downstream?
> Can you give a concrete example of how that breaks existing callers?
> Which tests cover the modified functions?
Either way the session accumulates conversation history, so follow-up questions can refer back to earlier answers.
show-config — inspect effective configuration
diffrag show-config
diffrag show-config --config /path/to/my.toml
Prints a table of every setting that is in effect, including defaults for keys not present in your config file.
db-path — print the database path for a repository
diffrag db-path
diffrag db-path --repo /path/to/repo
Prints the path where the vector index for that repository is (or will be) stored. Useful for inspecting or deleting a stale index.
create-config — create a starter config file
diffrag create-config # write to user-level global config
diffrag create-config --out .diffrag.toml # write a project-level override
diffrag create-config --force # overwrite if the file already exists
Creates a config file pre-populated with all available settings and their defaults. The destination directory is created automatically.
config-path — print the user-level config path
diffrag config-path
Prints the OS-appropriate path for the user-level config file (it may not exist
yet). Useful for opening the config in an editor: $EDITOR $(diffrag config-path).
Global options
Passed to diffrag itself, before the subcommand:
| Option | Description |
|---|---|
-v / --verbose |
Enable debug-level logging |
diffrag --verbose review --base main
Common command options
Accepted by review, ask, and (where noted) other subcommands:
| Option | Commands | Description |
|---|---|---|
-r / --repo DIRECTORY |
review, ask, db-path |
Path to the git repository (default .) |
-c / --config PATH |
review, ask, show-config |
Path to a .toml config file (overrides auto-discovery) |
-b / --base TEXT |
review, ask |
Base branch/ref. On ask, omit for codebase-only Q&A |
-H / --head TEXT |
review, ask |
Head branch/ref (default HEAD) |
-V / --verbosity |
review, ask |
brief | standard | detailed |
Configuration reference
All keys are optional. Unset keys fall back to the defaults shown below.
Place the file at $(diffrag config-path) for global defaults, or at .diffrag.toml
in a project root to override per-repository.
[ai]
# Base URL of the OpenAI-compatible or Ollama endpoint used for review generation.
# Do NOT include a /v1 suffix — it is appended automatically.
base_url = "http://localhost:11434"
# Model identifier sent with every completion request.
model = "llama3.2"
# Bearer token for the Authorization header. Leave empty for local servers.
api_key = ""
# Per-read timeout in seconds. Requests use streaming, so this is the maximum
# time allowed between consecutive chunks — not the total generation time.
# Increase only if the model stalls mid-generation for unusually long periods.
timeout = 120.0
[embedding]
# Endpoint used for computing text embeddings. Can be the same server as [ai]
# or a different one (e.g. a dedicated embedding service).
base_url = "http://localhost:11434"
# Embedding model. nomic-embed-text works well with Ollama.
model = "nomic-embed-text"
api_key = ""
timeout = 60.0
[indexing]
# The index is always stored in the OS user cache directory, keyed by repo.
# Run "diffrag db-path" to see the exact path. This cannot be configured.
# Maximum number of characters per text chunk when splitting files.
chunk_size = 1024
# Character overlap between consecutive chunks (helps preserve context at boundaries).
chunk_overlap = 128
# The repository's .gitignore is parsed automatically and its patterns are
# applied first. The list below provides additional excludes on top of .gitignore.
# Any path component matching one of these strings is also skipped.
excluded_patterns = [
".git", ".venv", "__pycache__", "node_modules",
".idea", ".mypy_cache", ".ruff_cache", "dist", "build"
]
# Files larger than this (in bytes) are silently skipped.
# Binary files are always skipped regardless of this setting.
max_file_size = 1000000
[review]
# Default base branch when --base is not specified on the command line.
base_branch = "main"
# Number of context chunks retrieved from the vector index per diff chunk.
# Higher values give the LLM more surrounding context but increase prompt size.
similarity_top_k = 5
# Approximate token budget per diff chunk before it is split into individual hunks.
# Reduce this if your model has a small context window.
max_prompt_tokens = 6000
# Detail level of generated reviews. Can be overridden per-run with --verbosity.
# "brief" — top 3 critical issues only, under 200 words
# "standard" — balanced default
# "detailed" — exhaustive, includes minor nits and suggested fixes per issue
verbosity = "standard"
# Maximum number of file reviews sent to the model simultaneously.
# Higher values cut wall-clock time on large changesets but raise peak API load;
# rate-limited (HTTP 429) requests are retried automatically with backoff.
# Set to 1 to restore fully sequential behaviour.
concurrency = 3
Library usage
diffrag is also usable as a Python library:
import asyncio
from pathlib import Path
from diffrag import (
Settings,
GitRepository,
OpenAICompatClient,
RepoIndexer,
CodeReviewer,
DiffResult,
default_db_path,
)
settings = Settings.from_toml(Path(".diffrag.toml"))
repo_path = Path("/path/to/repo")
ai_client = OpenAICompatClient(
base_url=settings.ai.base_url,
model=settings.ai.model,
api_key=settings.ai.api_key,
)
embed_client = OpenAICompatClient(
base_url=settings.embedding.base_url,
model=settings.embedding.model,
api_key=settings.embedding.api_key,
)
indexer = RepoIndexer(
# The index location is derived from the repo path; it is not a config key.
db_path=default_db_path(repo_path),
embedding_client=embed_client,
chunk_size=settings.indexing.chunk_size,
chunk_overlap=settings.indexing.chunk_overlap,
)
reviewer = CodeReviewer(
ai_client=ai_client,
indexer=indexer,
concurrency=settings.review.concurrency,
)
repo = GitRepository(repo_path)
async def main() -> None:
# review() checks the HEAD commit hash and only rebuilds the index when needed
result = await reviewer.review(repo, base="main", head="feature/my-feature")
print(result.summary)
# ask follow-up questions about the same diff; pass history for multi-turn
diff = repo.get_diff("main", "feature/my-feature")
history: list[tuple[str, str]] = []
q1 = "Are there any breaking API changes?"
a1 = await reviewer.ask(q1, diff)
print(a1)
history.append((q1, a1))
a2 = await reviewer.ask("Which callers are affected?", diff, history=history)
print(a2)
# ask about the codebase alone — pass an empty diff and refresh the index first
await reviewer.ensure_index(repo)
answer = await reviewer.ask(
"How does the indexer detect staleness?",
DiffResult(base_ref="", head_ref="", file_diffs=()),
)
print(answer)
asyncio.run(main())
For Ollama's native API (supports num_ctx and other Ollama-specific options):
from diffrag import OllamaClient
embed_client = OllamaClient(
base_url="http://localhost:11434",
model="nomic-embed-text",
)
How the review pipeline works
-
Diff —
git diff base...headis run for each changed file; results are parsed into typedFileDiff/Hunkobjects. -
Index — every non-binary file in the repository is split into overlapping character chunks and embedded into ChromaDB. The HEAD commit hash is saved alongside the index; on future runs this hash is compared and the rebuild is skipped if nothing has changed.
-
Per-file review — files are reviewed concurrently, up to
review.concurrencyat a time. For each changed file:- The LLM summarises the diff in plain English (used as a targeted search query).
- The summary is embedded and the top-k nearest chunks are retrieved from the index.
- The LLM reviews the raw diff in light of the retrieved context.
- Large diffs are split into individual git hunks and reviewed separately.
These three steps are sequential within a file because each depends on the previous one; the parallelism is across files. Results are collected in diff order regardless of which file finishes first.
-
Consolidation — all per-file reviews are sent to the LLM in a final prompt that produces a single, cross-cutting summary.
Development
# Install with dev dependencies
uv sync --group dev
# Run tests
uv run pytest
# Lint and format checks
uv run ruff check src tests
uv run ruff format --check src tests
# Type checking
uv run ty check src
# Run all checks via tox
uv run tox
# Build Sphinx docs
uv run sphinx-build docs docs/_build/html
Project structure
src/diffrag/
├── config/ # Settings dataclasses + TOML loading (settings.py)
├── diff/ # GitRepository, FileDiff, Hunk models (git.py, models.py)
├── ai/ # AIClient / EmbeddingClient protocols, OpenAI-compat + Ollama clients
├── indexing/ # ChromaDB-backed RepoIndexer with staleness detection
├── review/ # CodeReviewer orchestrator + prompt templates
└── cli/ # Click CLI: review / ask / show-config / create-config / db-path / config-path
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file diffrag-0.4.0.tar.gz.
File metadata
- Download URL: diffrag-0.4.0.tar.gz
- Upload date:
- Size: 33.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eea05ce34a1ff64032d31dac6000eff4b9331150eda97170dee2cec560537630
|
|
| MD5 |
72e0aa7d2a830af9edbbc3c70cfef1d7
|
|
| BLAKE2b-256 |
51849dbdd41cec124e04c37194856c1a33118f7b60df51ee7c36652101d13718
|
File details
Details for the file diffrag-0.4.0-py3-none-any.whl.
File metadata
- Download URL: diffrag-0.4.0-py3-none-any.whl
- Upload date:
- Size: 43.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bc605a2bef255c80641d609630b709559656205ed7a4dcace2baeb7bea0d59a
|
|
| MD5 |
621e7a34a8c83df9d1e24d783ea5fa61
|
|
| BLAKE2b-256 |
c53373281556a1f337f9a30f4e20e13b9752aa7233a31a3b000c6a34f5bfc464
|