blot-git 0.3.0__tar.gz → 0.3.1__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.3.0 → blot_git-0.3.1}/PKG-INFO +1 -1
- {blot_git-0.3.0 → blot_git-0.3.1}/pyproject.toml +1 -1
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/__init__.py +1 -1
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/index.py +37 -6
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_index.py +14 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.gitattributes +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.github/ISSUE_TEMPLATE/bench_case.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.github/ISSUE_TEMPLATE/check_idea.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.github/pull_request_template.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.github/workflows/ci.yml +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.github/workflows/release.yml +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.gitignore +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/.pre-commit-hooks.yaml +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/CONTRIBUTING.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/LICENSE +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/README.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/demo/demo.gif +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/demo/make_demo.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/alert.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/faces.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/happy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/idle.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/sad.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/sleepy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/walk.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/blue/worried.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/build_sprites.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/alert.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/faces.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/happy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/idle.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/sad.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/sleepy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/walk.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/mustard/worried.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/alert.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/faces.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/happy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/idle.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/sad.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/sleepy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/walk.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/orange/worried.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/alert.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/faces.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/happy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/idle.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/sad.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/sleepy.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/walk.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/assets/sora/sage/worried.svg +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/bench/README.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/bench/RESULTS.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/bench/cases.json +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/docs/EXPLAINER.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/docs/PRD.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/docs/PYPI.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/docs/competitors.md +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/__main__.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/bench.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/checks/__init__.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/checks/base.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/checks/cochange.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/checks/readded_bug.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/checks/removed_fix.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/classify.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/cli.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/config.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/diff.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/git.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/github.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/hooks.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/report.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/store.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/trace.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/src/blot/why.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/conftest.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_bench.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_checks.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_classify.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_cli.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_config.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_diff.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_git.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_hooks.py +0 -0
- {blot_git-0.3.0 → blot_git-0.3.1}/tests/test_why_github.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blot-git
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "blot-git"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1"
|
|
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" }
|
|
@@ -10,9 +10,11 @@ Targets: under 60 s for 50k commits on a laptop; incremental refresh under 1 s.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import os
|
|
13
14
|
import re
|
|
14
15
|
import subprocess
|
|
15
16
|
import time
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
16
18
|
from dataclasses import dataclass
|
|
17
19
|
|
|
18
20
|
from blot.classify import CommitInfo, fix_severity, is_refactor, linked_issues
|
|
@@ -85,12 +87,43 @@ def _head(repo: str, rev: str = "HEAD") -> str | None:
|
|
|
85
87
|
return None
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
_FORMAT = f"--format={_RECORD}%H{_FIELD}%aI{_FIELD}%s{_FIELD}%b{_FIELD}"
|
|
91
|
+
# Most of indexing is git computing --numstat line counts, one commit after another. Big histories
|
|
92
|
+
# are split into slices that separate git processes read in parallel (Django: 34k commits, ~170 s
|
|
93
|
+
# serially). The result is the same set of commits; order doesn't matter to the index.
|
|
94
|
+
_PARALLEL_FROM = 2000
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _workers() -> int:
|
|
98
|
+
return max(1, min(8, (os.cpu_count() or 2) - 1))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _git_stdin(repo: str, args: list[str], revs: list[str]) -> str:
|
|
102
|
+
done = subprocess.run(["git", "-C", repo, *args], input="\n".join(revs) + "\n", capture_output=True,
|
|
103
|
+
text=True, encoding="utf-8", errors="replace", check=True)
|
|
104
|
+
return done.stdout
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _read_history(repo: str, rev: str) -> list[tuple[CommitInfo, str, list[str]]]:
|
|
108
|
+
args = ["log", "--no-walk=unsorted", "--stdin", "-M", "--numstat", _FORMAT]
|
|
109
|
+
shas = run_git(repo, "rev-list", "--no-merges", rev).split()
|
|
110
|
+
if len(shas) < _PARALLEL_FROM:
|
|
111
|
+
return _parse_log(_git_stdin(repo, args, shas)) if shas else []
|
|
112
|
+
# interleaved slices have similar sizes; never pass an empty one (git log would fall back to HEAD)
|
|
113
|
+
slices = [part for i in range(_workers() * 2) if (part := shas[i::_workers() * 2])]
|
|
114
|
+
with ThreadPoolExecutor(_workers()) as pool:
|
|
115
|
+
outs = pool.map(lambda part: _git_stdin(repo, args, part), slices)
|
|
116
|
+
return [commit for out in outs for commit in _parse_log(out)]
|
|
117
|
+
|
|
118
|
+
|
|
88
119
|
def _removed_lines(repo: str, shas: list[str]) -> list[tuple[str, str, str]]:
|
|
89
120
|
"""(line_hash, path, sha) for every meaningful line the given commits deleted."""
|
|
121
|
+
batches = [shas[i : i + _SHOW_BATCH] for i in range(0, len(shas), _SHOW_BATCH)]
|
|
122
|
+
show = ["show", f"--format={_RECORD}%H", "-U0", "--no-color", "--no-ext-diff", "-M"]
|
|
123
|
+
with ThreadPoolExecutor(_workers()) as pool:
|
|
124
|
+
outs = list(pool.map(lambda batch: run_git(repo, *show, *batch), batches))
|
|
90
125
|
rows: list[tuple[str, str, str]] = []
|
|
91
|
-
for
|
|
92
|
-
batch = shas[i : i + _SHOW_BATCH]
|
|
93
|
-
out = run_git(repo, "show", f"--format={_RECORD}%H", "-U0", "--no-color", "--no-ext-diff", "-M", *batch)
|
|
126
|
+
for out in outs:
|
|
94
127
|
for chunk in out.split(_RECORD)[1:]:
|
|
95
128
|
sha, _, diff_text = chunk.partition("\n")
|
|
96
129
|
for change in parse_unified_diff(diff_text):
|
|
@@ -128,9 +161,7 @@ def build_index(
|
|
|
128
161
|
return IndexResult(0, 0, time.perf_counter() - start, rebuild)
|
|
129
162
|
|
|
130
163
|
rev = head if rebuild else f"{last}..{head}"
|
|
131
|
-
|
|
132
|
-
log = run_git(repo, "log", "--no-merges", "-M", "--numstat", fmt, rev)
|
|
133
|
-
parsed = _parse_log(log)
|
|
164
|
+
parsed = _read_history(repo, rev)
|
|
134
165
|
|
|
135
166
|
fix_shas = []
|
|
136
167
|
for info, date, paths in parsed:
|
|
@@ -66,6 +66,20 @@ def test_index_db_lives_in_git_dir(make_repo):
|
|
|
66
66
|
assert os.path.exists(repo / ".git" / "blot.db")
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def test_parallel_history_read_matches_serial(make_repo, tmp_path, monkeypatch):
|
|
70
|
+
import blot.index as ix
|
|
71
|
+
|
|
72
|
+
repo = make_repo(history())
|
|
73
|
+
build_index(str(repo), db_path=str(tmp_path / "s.db"))
|
|
74
|
+
monkeypatch.setattr(ix, "_PARALLEL_FROM", 1) # force slicing even for 3 commits
|
|
75
|
+
monkeypatch.setattr(ix, "_workers", lambda: 3) # 6 slices for 3 commits: some would be empty
|
|
76
|
+
build_index(str(repo), db_path=str(tmp_path / "p.db"))
|
|
77
|
+
a, b = Store.open(str(repo), str(tmp_path / "s.db")), Store.open(str(repo), str(tmp_path / "p.db"))
|
|
78
|
+
for table in ("commits", "fix_removed_lines", "file_commits", "cochange"):
|
|
79
|
+
rows = lambda s: sorted(s.conn.execute(f"SELECT * FROM {table}").fetchall()) # noqa: E731
|
|
80
|
+
assert rows(a) == rows(b), table
|
|
81
|
+
|
|
82
|
+
|
|
69
83
|
def test_empty_repo(tmp_path):
|
|
70
84
|
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
|
71
85
|
assert build_index(str(tmp_path)).commits == 0
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|