Metadata-Version: 2.4
Name: diffgate
Version: 0.1.0
Summary: Review the change, not the file. Catches skipped tests, swallowed exceptions, disabled verification and other quiet regressions in a diff.
Project-URL: Homepage, https://github.com/KozueMitarai/diffgate
Project-URL: Issues, https://github.com/KozueMitarai/diffgate/issues
Author: diffgate contributors
License-Expression: MIT
License-File: LICENSE
Keywords: ci,code-review,diff,git,linter,pre-commit,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Version Control :: Git
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# diffgate

**Review the change, not the file.**

`diffgate` reads a unified diff and reports the things that pass CI but should
not pass review: a test that just became skipped, an exception handler that just
became empty, certificate verification that just got turned off, an auth
decorator that quietly disappeared.

Zero dependencies. One command. Python 3.11+.

```console
$ diffgate

diffgate  3 file(s) changed, +12 -7

src/api/client.py
  error  provider API secret key committed in source  [security.hardcoded_secret]
         src/api/client.py:14
         │ self.api_key = api_key or "sk-l********"
         Move the value to an environment variable or a secret store. Anything
         committed to git must be treated as leaked, so rotate it as well as
         removing it.

  error  TLS certificate verification disabled  [security.tls_disabled]
         src/api/client.py:18
         │ return requests.get(path, timeout=10, verify=False)

  error  exception caught and ignored  [error.swallowed]
         src/api/client.py:19
         │ except Exception:

src/api/views.py
  error  `@login_required` was removed and does not appear in the new code  [security.guard_removed]
         src/api/views.py:5
         │ @login_required

tests/test_client.py
  error  pytest skip/xfail marker added  [test.skip_added]
         tests/test_client.py:4
         │ @pytest.mark.skip(reason="flaky")

5 errors
```

---

## Why a diff-aware tool

Your linter looks at the final state of a file. That makes it blind to a whole
category of change:

```python
@pytest.mark.skip(reason="flaky")     # ruff: fine. mypy: fine. diffgate: error.
def test_payment_is_captured():
    ...
```

Nothing is *wrong* with that file. Something is wrong with the **diff** that
introduced it. A skip marker that has been there for a year is somebody's known
trade-off; a skip marker added in this pull request is a decision being made
right now, and it deserves to be a deliberate one.

The same asymmetry runs through the whole rule set. `except Exception: pass`
that already existed is technical debt. `except Exception: pass` that appeared
in this change is a failing test being silenced. diffgate only reports the
second kind.

## Install

```bash
pip install diffgate          # or: pipx install diffgate / uv tool install diffgate
```

## Use

```bash
diffgate                      # uncommitted changes against HEAD
diffgate --staged             # what `git commit` would record
diffgate --base main          # this whole branch, the way a PR shows it
git diff | diffgate --stdin   # any diff from anywhere
```

Exit codes are CI-friendly: `0` clean, `1` findings at or above your threshold,
`2` diffgate itself could not run.

### As a pre-commit hook

```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/KozueMitarai/diffgate
    rev: v0.1.0
    hooks:
      - id: diffgate
```

### In GitHub Actions

Findings appear as inline annotations on the pull request diff.

```yaml
# .github/workflows/review.yml
name: review
on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  diffgate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # diffgate needs the base branch
      - uses: KozueMitarai/diffgate@v0.1.0
        with:
          fail-on: error
          comment: "true"       # also post the report as a PR comment
```

Or wire it up by hand, in any CI system:

```bash
pip install diffgate
diffgate --base "origin/$BASE_BRANCH" --format github     # inline annotations
diffgate --base "origin/$BASE_BRANCH" --format sarif -o diffgate.sarif
diffgate --base "origin/$BASE_BRANCH" --format markdown   # for a PR comment
```

Available formats: `text`, `json`, `markdown`, `github`, `sarif`.

## The rules

Run `diffgate --rules` for the current list with default severities.

| Rule | Default | What it catches |
|---|---|---|
| `test.skip_added` | error | `@pytest.mark.skip`, `it.skip`, `t.Skip()`, `#[ignore]`, `@Disabled` … |
| `test.focused` | error | `.only` / `fit` — leaves the rest of the suite unrun while CI stays green |
| `test.always_true` | error | `assert True`, `expect(true).toBe(true)` |
| `test.no_assertion` | warn | a new test whose body asserts nothing |
| `test.removed` | warn | more tests deleted than added |
| `error.swallowed` | error | `except: pass`, `catch {}`, `.catch(() => {})`, `_ = err` |
| `error.lint_suppressed` | warn | `# type: ignore`, `// @ts-ignore`, `# noqa`, `as any` |
| `security.tls_disabled` | error | `verify=False`, `rejectUnauthorized: false`, `InsecureSkipVerify` |
| `security.hardcoded_secret` | error | AWS/GitHub/Slack/provider keys, private keys, inline DB passwords |
| `security.guard_removed` | error | an auth decorator or permission check deleted and not replaced |
| `security.permissions_widened` | error | `chmod 777`, `privileged: true`, `0.0.0.0/0`, `"Principal": "*"` |
| `security.dangerous_exec` | warn | `shell=True`, `eval`, `pickle.loads`, `innerHTML =` |
| `security.weak_crypto` | warn | MD5/SHA-1 for hashing, ECB mode, `Math.random()` for tokens |
| `destructive.force_push` | error | `git push --force` without `--force-with-lease` |
| `destructive.sql` | error | `DROP TABLE`, `TRUNCATE`, `DELETE`/`UPDATE` with no `WHERE` |
| `destructive.command` | warn | `rm -rf`, `kubectl delete`, `terraform destroy` |
| `ci.check_disabled` | error | `continue-on-error: true`, `\|\| true`, `--exit-zero`, `[skip ci]` |
| `ci.coverage_lowered` | warn | a coverage or warning threshold moved down |
| `deps.added` | warn | a new third-party dependency |
| `deps.lockfile_stale` | warn | manifest changed, lockfile did not |
| `deps.pin_loosened` | warn | an exact pin replaced with a range |
| `scope.env_file` | error | a real `.env` committed (`.env.example` is fine) |
| `scope.migration_edited` | warn | an already-applied migration edited in place |
| `scope.generated_edited` | warn | a hand edit to a generated file |
| `scope.infra_changed` | info | Dockerfiles, Terraform, workflows, k8s manifests |
| `scope.large_diff` | info | more added lines than review can absorb |
| `scope.binary_added` | info | a binary blob whose contents nobody can review |
| `stub.not_implemented` | warn | `raise NotImplementedError`, `todo!()`, `panic("TODO")` |
| `stub.fake_return` | warn | a placeholder value returned as if it were real |
| `stub.todo_added` | info | a new TODO/FIXME |
| `debug.leftover` | warn | `console.log`, `breakpoint()`, `pdb.set_trace()`, `debugger` |
| `debug.print_statement` | off | bare `print` / `fmt.Println` (opt in for libraries) |
| `quality.commented_out_code` | info | code commented out instead of deleted |

## Configuration

Anything can be re-levelled or switched off. Start with:

```bash
diffgate --init
```

```toml
# .diffgate.toml
[diffgate]
fail_on = "error"          # error | warn | info | never
max_added_lines = 800
exclude = ["**/node_modules/**", "**/*.min.js"]

[rules]
"stub.todo_added" = "off"
"debug.print_statement" = "warn"
"deps.added" = "info"

[[exempt]]
paths = ["tests/fixtures/**"]
rules = ["security.hardcoded_secret"]
reason = "fixture credentials, not real"
```

Per-line, in the code itself:

```python
password = "not-really-a-secret"  # diffgate: ignore[security.hardcoded_secret]
```

```python
# diffgate: ignore-file
```

Bare `# diffgate: ignore` silences every rule on that line, and a marker on the
line *above* a finding works too.

## As a library

```python
from diffgate import analyze, load_config

result = analyze(open("change.diff").read(), load_config(None))
for finding in result.report.findings:
    print(finding.severity, finding.rule_id, finding.path, finding.line)
```

## Design notes

**False positives are the only thing that matters.** A rule that cries wolf gets
the whole tool switched off, which is worse than not shipping the rule. Patterns
are narrow on purpose, credible-but-noisy rules ship at `info`, and
`debug.print_statement` ships disabled. If diffgate is wrong about your code,
that is a bug — please report it.

**Secrets are redacted before they are printed.** A finding about a hardcoded key
would otherwise copy that key into your CI logs.

**No network, no telemetry, no dependencies.** It reads a diff and writes a
report.

**diffgate gates itself.** Its own CI runs `diffgate --base main` on every pull
request. Its `.diffgate.toml` exempts exactly two things — the file that *is* the
pattern list, and the test fixtures that must contain the code the rules detect —
each with a written reason.

## Contributing

```bash
git clone https://github.com/KozueMitarai/diffgate
cd diffgate
pip install -e ".[dev]"
pytest
```

A new rule needs three things: a narrow pattern, a test that it fires, and a
test that it does **not** fire on the nearest legitimate code. The second test
is the important one.

## License

MIT.

---

## About this project

diffgate was planned, designed, written and tested autonomously by an AI
(Claude) as part of an experiment in AI-run software projects. A human handles
account registration, payment setup and the decision to publish; every technical
choice in this repository is the AI's.

> This content is part of an experimental project planned and produced
> autonomously by an AI. No fact-checking was performed, and the accuracy of any
> information is not guaranteed.

That disclosure is about claims, not about code. The code makes no claims you
cannot check yourself: every rule ships with tests that demonstrate what it
catches and what it deliberately ignores (`pytest`), and diffgate runs against
its own diffs in CI. Nothing in this README asserts a measured improvement to
your bug rate, your review time, or anything else — no such measurement has been
made. Run it on your repository and judge the output.
