Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# diffly-cli
# diffly

[![CI](https://github.com/VIVAAN-DHAWAN/diffly-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/VIVAAN-DHAWAN/diffly-cli/actions/workflows/ci.yml)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Latest release](https://img.shields.io/github/v/release/VIVAAN-DHAWAN/diffly-cli)](https://github.com/VIVAAN-DHAWAN/diffly-cli/releases/latest)

**diffly-cli** is a deterministic, terminal-first triage tool for large GitHub pull requests. It fetches the pull request metadata, changed files, unified diff, commit metadata, status checks, and repository tree; parses supported source changes with Tree-sitter; identifies a conservative blast-radius map; applies fixed risk rules; and emits a one-page Markdown review with a `SHIP`, `QUARANTINE`, or `BLOCK` verdict.
**diffly** is a deterministic, terminal-first triage tool for large GitHub pull requests. It fetches the pull request metadata, changed files, unified diff, commit metadata, status checks, and repository tree; parses supported source changes with Tree-sitter; identifies a conservative blast-radius map; applies fixed risk rules; and emits a one-page Markdown review with a `PASS`, `QUARANTINE`, or `BLOCK` verdict.

This release includes **Phase 1 deterministic triage** plus the optional **Phase 2 literate-diff explainer**. Deterministic facts and verdicts remain authoritative; generated prose is clearly labeled and cannot change `SHIP`, `QUARANTINE`, or `BLOCK`.
This release includes **Phase 1 deterministic triage** plus the optional **Phase 2 literate-diff explainer**. Deterministic facts and verdicts remain authoritative; generated prose is clearly labeled and cannot change `PASS`, `QUARANTINE`, or `BLOCK`.

## At a glance

Expand All @@ -16,20 +16,21 @@ This release includes **Phase 1 deterministic triage** plus the optional **Phase
- **CI ready:** run it as a CLI, consume stable JSON, or add the bundled GitHub Action.
- **LLM optional:** generated explanation is isolated from the authoritative verdict and fails closed to deterministic output.
- **No GitHub CLI dependency:** it talks directly to the GitHub REST API.
- **Interactive terminal UI:** use arrow keys and the space bar to focus the report on what matters.

## Why it exists

Large AI-generated pull requests can be difficult to review because file-by-file diffs hide the affected symbols, adjacent tests, dependency changes, and high-risk surfaces. diffly-cli makes the deterministic part of that review visible before an LLM is introduced.

## Install

Requirements are Python 3.10 or newer. The installer creates an isolated environment under `~/.local/share/diffly-cli`, installs the package and its dependencies, and links the command into `~/.local/bin`.
Requirements are Python 3.10 or newer. The installer creates an isolated environment under `~/.local/share/diffly-cli`, installs the package and its dependencies, and links `diffly` into `~/.local/bin`.

```bash
curl -fsSL https://raw.githubusercontent.com/VIVAAN-DHAWAN/diffly-cli/main/install.sh | sh
```

That is the complete installation. If `diffly-cli` is not immediately found afterward, add `~/.local/bin` to your `PATH` and open a new shell. No GitHub CLI installation is required.
That is the complete installation. If `diffly` is not immediately found afterward, add `~/.local/bin` to your `PATH` and open a new shell. No GitHub CLI installation is required. The older `diffly-cli` executable remains as a compatibility alias.

A GitHub token is optional for light public use, but authenticated requests provide much higher API limits:

Expand Down Expand Up @@ -58,39 +59,54 @@ To enable the optional explainer in CI, add `DIFFLY_LLM_API_KEY: ${{ secrets.DIF

## Usage

The fastest way to start is simply:

```bash
diffly
```

This opens the guided terminal flow. Enter `owner/repo` (or a GitHub URL), the pull-request number, and whether you want an optional generated explanation. The flow validates inputs before making requests, never echoes tokens, and ends in the same keyboard-driven review screen as `--interactive`.

```bash
diffly-cli pr <owner/repo> <pr-number>
diffly pr <owner/repo> <pr-number>
```

Add the optional literate-diff explanation with an OpenAI-compatible BYOK endpoint:

```bash
export DIFFLY_LLM_API_KEY="your-key"
export DIFFLY_LLM_BASE_URL="https://api.openai.com/v1" # omit for the configured default endpoint
diffly-cli pr <owner/repo> <pr-number> --explain
diffly pr <owner/repo> <pr-number> --explain
```

The default model is `gpt-5-mini`; override it with `DIFFLY_LLM_MODEL` or `--llm-model`. The explainer sends bounded, redacted context, requires strict JSON output, rejects citations to files outside the changed-file set, and fails safely back to deterministic triage when no key is configured or the model response is invalid. Use `--json` with `--explain` to receive the structured literate-diff object.

Examples:

```bash
diffly-cli pr pallets/urllib3 3456
diffly-cli pr pallets/urllib3 3456 --output triage.md
diffly-cli pr pallets/urllib3 3456 --json
diffly pr pallets/urllib3 3456
diffly pr pallets/urllib3 3456 --interactive
diffly pr pallets/urllib3 3456 --output triage.md
diffly pr pallets/urllib3 3456 --json
diffly help
diffly setup
diffly doctor
diffly version
```

New to Diffly? Run `diffly setup` for a short guided tutorial covering PR analysis, interactive controls, automation, troubleshooting, credentials, and privacy. The final step can launch the real PR wizard so you can immediately try what you learned.

The command prints clean terminal Markdown by default. `--json` is provided for CI integration and `--output` saves the Markdown report to a file.

Run `diffly-cli pr --help` for the complete option reference. Pull-request numbers must be positive integers. For automation, prefer `--json`; operational errors exit with status `2` and successful triage exits with status `0` regardless of the verdict, so callers should inspect the `verdict` field when enforcing policy.
Run `diffly --help` or `diffly pr --help` for the complete option reference. `--interactive` opens a keyboard-driven selector: use up/down arrows to move, space to toggle sections, Enter to render, and `q` to quit. Pull-request numbers must be positive integers. For automation, prefer `--json`; operational errors exit with status `2` and successful triage exits with status `0` regardless of the verdict, so callers should inspect the `verdict` field when enforcing policy.

## Deterministic policy

| Verdict | Rule |
| --- | --- |
| **BLOCK** | A required check failed, or the pull request touches authentication, credentials, secrets, or security-sensitive files. |
| **QUARANTINE** | The pull request changes database schema or migrations, adds or changes dependencies, lacks obvious test coverage for a production file, or has unavailable/pending checks. |
| **SHIP** | No deterministic rule fired and observed checks passed. |
| **PASS** | No deterministic rule fired and observed checks passed. `SHIP` remains accepted as a legacy name in older integrations. |

The policy is deliberately conservative. A verdict is a review gate, not a claim that a pull request is correct or safe in every context.

Expand Down
20 changes: 14 additions & 6 deletions action/post_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def request(url: str, token: str, method: str = "GET", payload: dict | None = No
raw = response.read().decode()
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as exc:
if exc.code == 404 and method == "GET":
return []
detail = exc.read().decode(errors="replace")
raise SystemExit(f"GitHub API {exc.code}: {detail[:500]}") from exc

Expand All @@ -50,12 +52,18 @@ def main() -> int:
break
page += 1
match = next((item for item in existing if MARKER in (item.get("body") or "")), None)
if match:
request(f"{base}/{match['id']}", token, "PATCH", {"body": body})
print(f"Updated Diffly comment {match['id']}")
else:
created = request(base, token, "POST", {"body": body})
print(f"Created Diffly comment {created.get('id', 'unknown')}")
try:
if match:
request(f"{base}/{match['id']}", token, "PATCH", {"body": body})
print(f"Updated Diffly comment {match['id']}")
else:
created = request(base, token, "POST", {"body": body})
print(f"Created Diffly comment {created.get('id', 'unknown')}")
except SystemExit as exc:
if "GitHub API 404" in str(exc):
print(f"Warning: Diffly could not publish its PR comment (GitHub returned 404; check pull-request write permissions).")
else:
raise
return 0


Expand Down
8 changes: 5 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ python3 -m venv "${DIFFLY_HOME}"
"${DIFFLY_HOME}/bin/python" -m pip install --upgrade pip >/dev/null
"${DIFFLY_HOME}/bin/python" -m pip install --upgrade "${DIFFLY_SOURCE}"
mkdir -p "${DIFFLY_BIN}"
ln -sf "${DIFFLY_HOME}/bin/diffly-cli" "${DIFFLY_BIN}/diffly-cli"
ln -sf "${DIFFLY_HOME}/bin/diffly" "${DIFFLY_BIN}/diffly"
# Keep the old executable as a compatibility alias for existing scripts.
ln -sf "${DIFFLY_HOME}/bin/diffly" "${DIFFLY_BIN}/diffly-cli"

printf '\nInstalled diffly-cli.\n'
printf 'Run: diffly-cli pr <owner/repo> <pr-number>\n'
printf '\nInstalled diffly.\n'
printf 'Run: diffly pr <owner/repo> <pr-number>\n'
case ":${PATH}:" in
*":${DIFFLY_BIN}:"*) ;;
*) printf 'If the command is not found, add %s to PATH.\n' "${DIFFLY_BIN}" ;;
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
]

[project.scripts]
diffly = "diffly_cli.cli:main"
diffly-cli = "diffly_cli.cli:main"

[project.optional-dependencies]
Expand Down
Loading
Loading