blot-git 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blot_git-0.4.0/.github/workflows/blot.yml +14 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.pre-commit-hooks.yaml +1 -1
- {blot_git-0.3.2 → blot_git-0.4.0}/PKG-INFO +1 -1
- {blot_git-0.3.2 → blot_git-0.4.0}/README.md +21 -0
- blot_git-0.4.0/action.yml +73 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/pyproject.toml +1 -1
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/__init__.py +1 -1
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/cli.py +3 -1
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/diff.py +10 -0
- blot_git-0.4.0/tests/test_action.py +67 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_cli.py +22 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.gitattributes +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.github/ISSUE_TEMPLATE/bench_case.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.github/ISSUE_TEMPLATE/check_idea.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.github/pull_request_template.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.github/workflows/ci.yml +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.github/workflows/release.yml +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/.gitignore +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/CONTRIBUTING.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/LICENSE +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/demo/demo.gif +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/demo/make_demo.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/alert.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/faces.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/happy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/idle.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/sad.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/sleepy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/walk.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/blue/worried.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/build_sprites.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/alert.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/faces.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/happy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/idle.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/sad.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/sleepy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/walk.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/mustard/worried.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/alert.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/faces.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/happy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/idle.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/sad.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/sleepy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/walk.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/orange/worried.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/alert.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/faces.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/happy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/idle.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/sad.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/sleepy.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/walk.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/assets/sora/sage/worried.svg +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/bench/README.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/bench/RESULTS.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/bench/cases.json +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/docs/EXPLAINER.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/docs/PRD.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/docs/PYPI.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/docs/competitors.md +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/__main__.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/bench.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/checks/__init__.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/checks/base.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/checks/cochange.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/checks/readded_bug.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/checks/removed_fix.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/classify.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/config.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/git.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/github.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/hooks.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/index.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/report.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/store.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/trace.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/src/blot/why.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/conftest.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_bench.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_checks.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_classify.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_config.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_diff.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_git.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_hooks.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_index.py +0 -0
- {blot_git-0.3.2 → blot_git-0.4.0}/tests/test_why_github.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: Blot on Blot
|
|
2
|
+
|
|
3
|
+
# Runs this repo's own action (action.yml) on every pull request, installing Blot from the
|
|
4
|
+
# pull request's code: it tests the action end to end and reviews Blot's history with Blot.
|
|
5
|
+
on: pull_request
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
blot:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: ./
|
|
13
|
+
with:
|
|
14
|
+
blot-package: .
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blot-git
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A pre-commit check that has read your project's history: warns when a change removes a fix, re-adds a fixed bug, or forgets a file that always changes too.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Sym-jay/Blot
|
|
6
6
|
Project-URL: Issues, https://github.com/Sym-jay/Blot/issues
|
|
@@ -79,6 +79,27 @@ blot index --github
|
|
|
79
79
|
|
|
80
80
|
With the [GitHub CLI](https://cli.github.com) logged in, Blot fetches the titles and labels of the issues and PRs your fix commits mention, so warnings read `fixes #2935 "copy_current_request_context copies old session object"` (a real Flask issue) instead of a bare number. A `bug` label makes a commit count as a load-bearing fix; a fix-looking commit whose issues are only labelled `enhancement` or `documentation` is demoted. Refreshes only fetch what's new. Repos with GitHub Issues turned off (they track bugs elsewhere) are skipped.
|
|
81
81
|
|
|
82
|
+
## Use in CI
|
|
83
|
+
|
|
84
|
+
Run Blot on every pull request and see its findings as warnings on the changed lines:
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
# .github/workflows/blot.yml
|
|
88
|
+
name: Blot
|
|
89
|
+
on: pull_request
|
|
90
|
+
jobs:
|
|
91
|
+
blot:
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
steps:
|
|
94
|
+
- uses: Sym-jay/Blot@v0.4.0
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The action checks out the pull request with its full history, installs Blot, runs `blot index` and
|
|
98
|
+
`blot check --range origin/<base>..HEAD --format json`, and turns each finding into a warning annotation.
|
|
99
|
+
It never fails the job because of findings. Inputs: `blot-package` (what to `pip install`; default
|
|
100
|
+
`blot-git`, the latest PyPI release), `python-version` (default `3.13`) and `base-ref`
|
|
101
|
+
(default: the pull request's base branch).
|
|
102
|
+
|
|
82
103
|
## Measure it on your repo
|
|
83
104
|
|
|
84
105
|
```bash
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Run Blot on a pull request and show its findings as warning annotations.
|
|
2
|
+
# Usage (in a workflow triggered by pull_request):
|
|
3
|
+
# - uses: Sym-jay/Blot@v0.4.0
|
|
4
|
+
# See "Use in CI" in README.md.
|
|
5
|
+
name: Blot
|
|
6
|
+
description: History-aware checks on the commits of a pull request, shown as warning annotations
|
|
7
|
+
author: Sym-jay
|
|
8
|
+
branding:
|
|
9
|
+
icon: alert-triangle
|
|
10
|
+
color: orange
|
|
11
|
+
|
|
12
|
+
inputs:
|
|
13
|
+
base-ref:
|
|
14
|
+
description: Branch the pull request merges into; the action checks origin/<base-ref>..HEAD
|
|
15
|
+
default: ${{ github.base_ref }}
|
|
16
|
+
python-version:
|
|
17
|
+
description: Python version used to run Blot
|
|
18
|
+
default: "3.13"
|
|
19
|
+
blot-package:
|
|
20
|
+
description: What to pip install (a pip requirement, e.g. blot-git==0.3.2)
|
|
21
|
+
default: blot-git # the latest release on PyPI
|
|
22
|
+
|
|
23
|
+
runs:
|
|
24
|
+
using: composite
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
with:
|
|
28
|
+
fetch-depth: 0 # Blot reads the whole history
|
|
29
|
+
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ inputs.python-version }}
|
|
33
|
+
|
|
34
|
+
- name: Install Blot
|
|
35
|
+
shell: bash
|
|
36
|
+
env:
|
|
37
|
+
BLOT_PACKAGE: ${{ inputs.blot-package }}
|
|
38
|
+
run: python -m pip install --disable-pip-version-check "$BLOT_PACKAGE"
|
|
39
|
+
|
|
40
|
+
- name: Run Blot
|
|
41
|
+
shell: bash
|
|
42
|
+
env:
|
|
43
|
+
BASE_REF: ${{ inputs.base-ref }}
|
|
44
|
+
BLOT_FINDINGS: ${{ runner.temp }}/blot-findings.json
|
|
45
|
+
PYTHONUTF8: "1" # commit subjects in findings may not be ASCII
|
|
46
|
+
run: |
|
|
47
|
+
if [[ -z "$BASE_REF" ]]; then
|
|
48
|
+
echo "::error title=blot::No base branch: run this action on pull_request, or set base-ref"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
blot index
|
|
52
|
+
blot check --range "origin/${BASE_REF}..HEAD" --format json > "$BLOT_FINDINGS"
|
|
53
|
+
python - <<'PY'
|
|
54
|
+
# Turn blot's JSON findings into workflow annotations (::warning file=...,line=...::message).
|
|
55
|
+
import json, os
|
|
56
|
+
|
|
57
|
+
def esc(text, prop=False):
|
|
58
|
+
text = text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
|
|
59
|
+
return text.replace(":", "%3A").replace(",", "%2C") if prop else text
|
|
60
|
+
|
|
61
|
+
with open(os.environ["BLOT_FINDINGS"], encoding="utf-8") as f:
|
|
62
|
+
raw = f.read()
|
|
63
|
+
# blot check prints nothing on stdout when it hits an internal error (the reason is in the log above)
|
|
64
|
+
findings = json.loads(raw) if raw.strip() else []
|
|
65
|
+
for f in findings:
|
|
66
|
+
props = f"file={esc(f['path'], True)}"
|
|
67
|
+
if f.get("line"):
|
|
68
|
+
props += f",line={f['line']}"
|
|
69
|
+
props += f",title={esc('blot: ' + f['check'], True)}"
|
|
70
|
+
message = "\n".join([f["message"], *f.get("evidence", [])])
|
|
71
|
+
print(f"::warning {props}::{esc(message)}")
|
|
72
|
+
print(f"blot: {len(findings)} finding{'' if len(findings) == 1 else 's'}")
|
|
73
|
+
PY
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "blot-git"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "A pre-commit check that has read your project's history: warns when a change removes a fix, re-adds a fixed bug, or forgets a file that always changes too."
|
|
5
5
|
readme = "docs/PYPI.md" # text-only; the GitHub README's relative Sora images break on PyPI
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -60,12 +60,14 @@ def _open_index(repo: str):
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def _cmd_check(args: argparse.Namespace) -> int:
|
|
63
|
-
from blot.diff import read_changes
|
|
63
|
+
from blot.diff import range_base, read_changes
|
|
64
64
|
|
|
65
65
|
try:
|
|
66
66
|
config = load_config(args.repo)
|
|
67
67
|
store = _open_index(args.repo)
|
|
68
68
|
changes = read_changes(args.repo, rev_range=args.range)
|
|
69
|
+
if args.range: # removed lines are numbered as in the range's base, not as in HEAD
|
|
70
|
+
store.rev = range_base(args.repo, args.range)
|
|
69
71
|
findings = [f for check in all_checks(config) for f in check.run(changes, store, config)]
|
|
70
72
|
except Exception as exc: # Blot's own failures must never block a commit
|
|
71
73
|
print(f"blot: internal error, skipping checks ({exc.__class__.__name__}: {exc})", file=sys.stderr)
|
|
@@ -145,3 +145,13 @@ def read_changes(repo: str, rev_range: str | None = None) -> list[FileChange]:
|
|
|
145
145
|
args = ["diff", "-U0", "--no-color", "--no-ext-diff", "-M"]
|
|
146
146
|
args.append(rev_range if rev_range else "--cached")
|
|
147
147
|
return parse_unified_diff(run_git(repo, *args))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def range_base(repo: str, rev_range: str) -> str:
|
|
151
|
+
"""The commit `git diff <rev_range>` compares against: A for `A..B`, the merge base for `A...B`."""
|
|
152
|
+
if "..." in rev_range:
|
|
153
|
+
left, right = rev_range.split("...", 1)
|
|
154
|
+
return run_git(repo, "merge-base", left or "HEAD", right or "HEAD").strip()
|
|
155
|
+
if ".." in rev_range:
|
|
156
|
+
return rev_range.split("..", 1)[0] or "HEAD"
|
|
157
|
+
return rev_range
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""action.yml: the step that turns `blot check --format json` output into workflow annotations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import textwrap
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from blot.cli import main
|
|
11
|
+
|
|
12
|
+
ACTION = Path(__file__).resolve().parent.parent / "action.yml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _annotate_script() -> str:
|
|
16
|
+
"""The Python heredoc in action.yml's "Run Blot" step (stdlib only, so no YAML parser needed)."""
|
|
17
|
+
lines = ACTION.read_text(encoding="utf-8").splitlines()
|
|
18
|
+
start = next(i for i, line in enumerate(lines) if line.strip() == "python - <<'PY'") + 1
|
|
19
|
+
end = next(i for i in range(start, len(lines)) if lines[i].strip() == "PY")
|
|
20
|
+
return textwrap.dedent("\n".join(lines[start:end])) + "\n"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _annotate(tmp_path: Path, raw: str) -> list[str]:
|
|
24
|
+
findings = tmp_path / "findings.json"
|
|
25
|
+
findings.write_text(raw, encoding="utf-8")
|
|
26
|
+
out = subprocess.run([sys.executable, "-c", _annotate_script()], env={**os.environ, "BLOT_FINDINGS": str(findings)},
|
|
27
|
+
capture_output=True, text=True, check=True).stdout
|
|
28
|
+
return out.splitlines()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_findings_become_warning_annotations(tmp_path):
|
|
32
|
+
findings = [
|
|
33
|
+
{"check": "removed-fix", "path": "src/a,b.py", "line": 12, "severity": 2,
|
|
34
|
+
"message": "You're changing a line added by a bug fix: 100% sure.",
|
|
35
|
+
"evidence": ['8efea0cc "Break reference cycle" (2020-10-03, fixes #3761)']},
|
|
36
|
+
{"check": "forgot-related-file", "path": "app.py", "line": None, "severity": 1,
|
|
37
|
+
"message": "app.py usually changes with views.py.", "evidence": []},
|
|
38
|
+
]
|
|
39
|
+
assert _annotate(tmp_path, json.dumps(findings)) == [
|
|
40
|
+
"::warning file=src/a%2Cb.py,line=12,title=blot%3A removed-fix::"
|
|
41
|
+
"You're changing a line added by a bug fix: 100%25 sure."
|
|
42
|
+
'%0A8efea0cc "Break reference cycle" (2020-10-03, fixes #3761)',
|
|
43
|
+
"::warning file=app.py,title=blot%3A forgot-related-file::app.py usually changes with views.py.",
|
|
44
|
+
"blot: 2 findings",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_no_findings_and_no_output(tmp_path):
|
|
49
|
+
assert _annotate(tmp_path, "[]\n") == ["blot: 0 findings"]
|
|
50
|
+
assert _annotate(tmp_path, "") == ["blot: 0 findings"] # blot check hit an internal error
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_range_check_of_a_pr_branch_is_annotated(make_repo, tmp_path, capsys):
|
|
54
|
+
from test_checks import AFTER, BEFORE, GUARD
|
|
55
|
+
|
|
56
|
+
repo = make_repo([("Add app", {"app.py": BEFORE}),
|
|
57
|
+
("Break reference cycle in App (fixes #3761)", {"app.py": AFTER})])
|
|
58
|
+
git = lambda *args: subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True) # noqa: E731
|
|
59
|
+
git("switch", "-q", "-c", "pr")
|
|
60
|
+
(repo / "app.py").write_text(AFTER.replace(GUARD, "self_ref = self"), encoding="utf-8")
|
|
61
|
+
git("commit", "-q", "-am", "Simplify App")
|
|
62
|
+
|
|
63
|
+
assert main(["--repo", str(repo), "check", "--range", "main..HEAD", "--format", "json"]) == 0
|
|
64
|
+
lines = _annotate(tmp_path, capsys.readouterr().out)
|
|
65
|
+
assert lines[-1].startswith("blot: ") and lines[-1] != "blot: 0 findings"
|
|
66
|
+
assert any(line.startswith("::warning file=app.py,line=") and ",title=blot%3A removed-fix::" in line
|
|
67
|
+
for line in lines)
|
|
@@ -78,3 +78,25 @@ def test_removed_fix_traces_past_refactor(make_repo, capsys):
|
|
|
78
78
|
# plain blame stops at the split; blot must cite the fix behind it
|
|
79
79
|
assert finding["evidence"][0].startswith(sha("HEAD~1")[:7] + ' "Break reference cycle in App (fixes #3761)"')
|
|
80
80
|
assert finding["evidence"][1].startswith("traced past refactor " + sha("HEAD")[:7])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_check_range_reads_removed_lines_at_the_base(make_repo, capsys):
|
|
84
|
+
"""In CI the PR's commits are already in the index and HEAD no longer has the removed line."""
|
|
85
|
+
from test_checks import AFTER, BEFORE, GUARD
|
|
86
|
+
|
|
87
|
+
repo = make_repo([("Add app", {"app.py": BEFORE}),
|
|
88
|
+
("Break reference cycle in App (fixes #3761)", {"app.py": AFTER})])
|
|
89
|
+
git = lambda *args: subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True) # noqa: E731
|
|
90
|
+
git("switch", "-q", "-c", "pr")
|
|
91
|
+
(repo / "app.py").write_text(AFTER.replace(GUARD, "self_ref = self"), encoding="utf-8")
|
|
92
|
+
git("commit", "-q", "-am", "Simplify App")
|
|
93
|
+
git("switch", "-q", "main")
|
|
94
|
+
(repo / "README.md").write_text("app\n", encoding="utf-8")
|
|
95
|
+
git("add", "README.md")
|
|
96
|
+
git("commit", "-q", "-m", "docs: readme") # main moves on, so A..B and A...B differ
|
|
97
|
+
git("switch", "-q", "pr")
|
|
98
|
+
|
|
99
|
+
for rev_range in ("HEAD~1..HEAD", "main...pr"):
|
|
100
|
+
assert main(["--repo", str(repo), "check", "--range", rev_range, "--format", "json"]) == 0
|
|
101
|
+
found = [(f["check"], f["path"], f["line"]) for f in json.loads(capsys.readouterr().out)]
|
|
102
|
+
assert ("removed-fix", "app.py", 3) in found, rev_range
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|