git-bisectlib 0.16.1__tar.gz → 0.16.2__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.
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/PKG-INFO +3 -3
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/README.md +2 -2
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/bisectlib/__init__.py +2 -2
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/bisectlib/_report.py +25 -20
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/git_bisectlib.egg-info/PKG-INFO +3 -3
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/pyproject.toml +1 -1
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/tests/test_report.py +11 -11
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/LICENSE +0 -0
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/bisectlib/py.typed +0 -0
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/git_bisectlib.egg-info/SOURCES.txt +0 -0
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/git_bisectlib.egg-info/dependency_links.txt +0 -0
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/git_bisectlib.egg-info/top_level.txt +0 -0
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/setup.cfg +0 -0
- {git_bisectlib-0.16.1 → git_bisectlib-0.16.2}/tests/test_bisectlib.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git_bisectlib
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Write tiny git bisect recipes in Python; watch progress live in .bisect/status.md.
|
|
5
5
|
Author-email: Martin Leitner-Ankerl <martin.ankerl@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -152,7 +152,7 @@ Date: 2026-06-15 11:40:00 +0200
|
|
|
152
152
|
1 file changed, 6 insertions(+), 6 deletions(-)
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
| good | bad |
|
|
155
|
+
| good | bad | probe | range | status |
|
|
156
156
|
|------|-----|----------|-------|--------|
|
|
157
157
|
| `2801e9572` …, Bob | `79cb050c2` …, Alice | `cb5394973` …, Carol | 27d 15h · 11 commits | 🟢 good |
|
|
158
158
|
| `cb5394973` …, Carol | `79cb050c2` …, Alice | `95345541b` …, Dan | 12d 7h · 6 commits | 🔴 bad · 81.2s |
|
|
@@ -160,7 +160,7 @@ Date: 2026-06-15 11:40:00 +0200
|
|
|
160
160
|
| `cb5394973` …, Carol | `5c9dcafb3` …, Eve | `19d89b121` …, Fay | 3d 5h · 2 commits | 🟢 good |
|
|
161
161
|
````
|
|
162
162
|
|
|
163
|
-
Each row reads in causal order — the **input range** (`good`/`bad`) → the **
|
|
163
|
+
Each row reads in causal order — the **input range** (`good`/`bad`) → the **probe** git
|
|
164
164
|
chose → the **result** — so you see the range funnel down as you scan. The report is
|
|
165
165
|
re-rendered the moment each command *starts*, links every step to its live-streamed log
|
|
166
166
|
under `.bisect/<sha>/`, and — when the search resolves — shows the culprit the way
|
|
@@ -135,7 +135,7 @@ Date: 2026-06-15 11:40:00 +0200
|
|
|
135
135
|
1 file changed, 6 insertions(+), 6 deletions(-)
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
| good | bad |
|
|
138
|
+
| good | bad | probe | range | status |
|
|
139
139
|
|------|-----|----------|-------|--------|
|
|
140
140
|
| `2801e9572` …, Bob | `79cb050c2` …, Alice | `cb5394973` …, Carol | 27d 15h · 11 commits | 🟢 good |
|
|
141
141
|
| `cb5394973` …, Carol | `79cb050c2` …, Alice | `95345541b` …, Dan | 12d 7h · 6 commits | 🔴 bad · 81.2s |
|
|
@@ -143,7 +143,7 @@ Date: 2026-06-15 11:40:00 +0200
|
|
|
143
143
|
| `cb5394973` …, Carol | `5c9dcafb3` …, Eve | `19d89b121` …, Fay | 3d 5h · 2 commits | 🟢 good |
|
|
144
144
|
````
|
|
145
145
|
|
|
146
|
-
Each row reads in causal order — the **input range** (`good`/`bad`) → the **
|
|
146
|
+
Each row reads in causal order — the **input range** (`good`/`bad`) → the **probe** git
|
|
147
147
|
chose → the **result** — so you see the range funnel down as you scan. The report is
|
|
148
148
|
re-rendered the moment each command *starts*, links every step to its live-streamed log
|
|
149
149
|
under `.bisect/<sha>/`, and — when the search resolves — shows the culprit the way
|
|
@@ -65,7 +65,7 @@ from typing import Callable, Literal, NoReturn, Optional, Union
|
|
|
65
65
|
_BadWhen = Literal["fail", "pass"]
|
|
66
66
|
_OnTimeout = Literal["abort", "skip", "bad"]
|
|
67
67
|
|
|
68
|
-
__version__ = "0.16.
|
|
68
|
+
__version__ = "0.16.2"
|
|
69
69
|
|
|
70
70
|
__all__ = [
|
|
71
71
|
"run", "test", "hammer", "check", # the verbs
|
|
@@ -1428,7 +1428,7 @@ def replace(path: str, old: Union[str, "re.Pattern"], new: str, *,
|
|
|
1428
1428
|
old_str = old.pattern if isinstance(old, re.Pattern) else str(old)
|
|
1429
1429
|
_final.setdefault("fixups", []).append(
|
|
1430
1430
|
{"kind": "replace", "path": path,
|
|
1431
|
-
"
|
|
1431
|
+
"old": old_str, "new": new})
|
|
1432
1432
|
sys.stderr.write(f" edit {path}: {n} replacement(s)\n")
|
|
1433
1433
|
|
|
1434
1434
|
|
|
@@ -123,7 +123,7 @@ class Sidecar:
|
|
|
123
123
|
class Row:
|
|
124
124
|
bad: str # input-range bad bound (sha) before this evaluation
|
|
125
125
|
good: str # input-range good bound (sha) before this evaluation
|
|
126
|
-
|
|
126
|
+
probe: str # the commit evaluated this step
|
|
127
127
|
status: str # good | bad | skip | todo
|
|
128
128
|
n_commits: int = 0 # candidate commits still in range at this step
|
|
129
129
|
span_seconds: int = 0 # wall span between good and bad commit dates
|
|
@@ -229,7 +229,7 @@ def build_report(
|
|
|
229
229
|
current_good: Optional[str] = None
|
|
230
230
|
active_goods: list[str] = [] # every good marked so far (defines the range)
|
|
231
231
|
rows: list[Row] = []
|
|
232
|
-
|
|
232
|
+
seen_probes: set[str] = set()
|
|
233
233
|
|
|
234
234
|
def ready() -> bool:
|
|
235
235
|
return current_bad is not None and current_good is not None
|
|
@@ -259,17 +259,17 @@ def build_report(
|
|
|
259
259
|
if current_good is None or is_ancestor(repo, current_good, sha):
|
|
260
260
|
current_good = sha
|
|
261
261
|
|
|
262
|
-
def add_row(
|
|
262
|
+
def add_row(probe: str, status: str) -> None:
|
|
263
263
|
rows.append(
|
|
264
264
|
Row(
|
|
265
265
|
bad=current_bad,
|
|
266
266
|
good=current_good,
|
|
267
|
-
|
|
267
|
+
probe=probe,
|
|
268
268
|
status=status,
|
|
269
269
|
goods=list(active_goods),
|
|
270
270
|
)
|
|
271
271
|
)
|
|
272
|
-
|
|
272
|
+
seen_probes.add(probe)
|
|
273
273
|
|
|
274
274
|
for verb, args in ops:
|
|
275
275
|
revs = [a for a in args if not a.startswith("-")]
|
|
@@ -338,14 +338,14 @@ def build_report(
|
|
|
338
338
|
if n_remaining <= 1:
|
|
339
339
|
first_bad = current_bad
|
|
340
340
|
|
|
341
|
-
# In-flight row: HEAD is the
|
|
341
|
+
# In-flight row: HEAD is the probe git currently has checked out, awaiting a
|
|
342
342
|
# verdict, and is not yet a logged marking.
|
|
343
343
|
in_progress = False
|
|
344
344
|
if (
|
|
345
345
|
ready()
|
|
346
346
|
and first_bad is None
|
|
347
347
|
and head
|
|
348
|
-
and head not in
|
|
348
|
+
and head not in seen_probes
|
|
349
349
|
and head != current_good
|
|
350
350
|
):
|
|
351
351
|
within_range = (
|
|
@@ -355,7 +355,7 @@ def build_report(
|
|
|
355
355
|
# A per-commit sidecar written by the engine is proof HEAD is the commit
|
|
356
356
|
# being evaluated right now — trust it when the ancestry checks can't
|
|
357
357
|
# confirm the range (shallow/grafted clone, or an anchor good that isn't a
|
|
358
|
-
# topological ancestor of the
|
|
358
|
+
# topological ancestor of the probe). This keeps status.md live (the
|
|
359
359
|
# in-flight row and its steps refresh after every command) on such repos.
|
|
360
360
|
has_sidecar = bool(logs_dir) and (Path(logs_dir) / head / "eval.json").is_file()
|
|
361
361
|
if within_range or has_sidecar:
|
|
@@ -365,7 +365,7 @@ def build_report(
|
|
|
365
365
|
# Gather subjects + dates for every sha we reference.
|
|
366
366
|
shas = set()
|
|
367
367
|
for r in rows:
|
|
368
|
-
shas.update([r.bad, r.good, r.
|
|
368
|
+
shas.update([r.bad, r.good, r.probe])
|
|
369
369
|
shas.update([orig_bad, current_bad, current_good, *orig_goods])
|
|
370
370
|
shas.discard(None)
|
|
371
371
|
subjects, dates, authors = _commit_meta(repo, shas)
|
|
@@ -381,7 +381,7 @@ def build_report(
|
|
|
381
381
|
r.bad_date = dates.get(r.bad, "")
|
|
382
382
|
r.span_seconds = _date_delta_seconds(r.good_date, r.bad_date)
|
|
383
383
|
if logs_dir:
|
|
384
|
-
r.sidecar = _load_sidecar(logs_dir, r.
|
|
384
|
+
r.sidecar = _load_sidecar(logs_dir, r.probe)
|
|
385
385
|
|
|
386
386
|
# The in-flight commit (HEAD) is a `todo` row because git hasn't recorded its
|
|
387
387
|
# mark yet — but if the recipe already finished and its sidecar carries a
|
|
@@ -640,7 +640,7 @@ def render_markdown(rep: Report, details: bool = False, color: bool = True) -> s
|
|
|
640
640
|
lines.append(f"> ⚠️ {rep.note}")
|
|
641
641
|
lines.append("")
|
|
642
642
|
|
|
643
|
-
lines.append("| good | bad |
|
|
643
|
+
lines.append("| good | bad | probe | range | status |")
|
|
644
644
|
lines.append("|------|-----|----------|-------|--------|")
|
|
645
645
|
for r in rep.rows:
|
|
646
646
|
rng = f"{fmt_duration(r.span_seconds)} · {r.n_commits} commits"
|
|
@@ -649,7 +649,7 @@ def render_markdown(rep: Report, details: bool = False, color: bool = True) -> s
|
|
|
649
649
|
if extra:
|
|
650
650
|
status += f" · {extra}"
|
|
651
651
|
lines.append(
|
|
652
|
-
f"| {cell(r.good)} | {cell(r.bad)} | {cell(r.
|
|
652
|
+
f"| {cell(r.good)} | {cell(r.bad)} | {cell(r.probe)} | {rng} | {status} |"
|
|
653
653
|
)
|
|
654
654
|
lines.append("")
|
|
655
655
|
|
|
@@ -660,15 +660,20 @@ def render_markdown(rep: Report, details: bool = False, color: bool = True) -> s
|
|
|
660
660
|
lines.append("")
|
|
661
661
|
for r in detail_rows:
|
|
662
662
|
lines.append(
|
|
663
|
-
f"### `{rep.short(r.
|
|
663
|
+
f"### `{rep.short(r.probe)}` — {rep.subject(r.probe)} "
|
|
664
664
|
f"({icon(r.status)})"
|
|
665
665
|
)
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
666
|
+
for f in r.sidecar.fixups:
|
|
667
|
+
kind = f.get("kind", "")
|
|
668
|
+
if kind == "replace":
|
|
669
|
+
lines.append(
|
|
670
|
+
f"- replace: in `{f['path']}`: "
|
|
671
|
+
f"`{f['old']}` → `{f['new']}`"
|
|
672
|
+
)
|
|
673
|
+
else:
|
|
674
|
+
lines.append(
|
|
675
|
+
f"- {kind}: `{f.get('detail', f.get('path', ''))}`"
|
|
676
|
+
)
|
|
672
677
|
for s in r.sidecar.steps:
|
|
673
678
|
if s.verb != "hammer":
|
|
674
679
|
continue
|
|
@@ -693,7 +698,7 @@ def render_markdown(rep: Report, details: bool = False, color: bool = True) -> s
|
|
|
693
698
|
# link the step to its captured log file (relative to status.md,
|
|
694
699
|
# which sits alongside the per-commit <sha>/ log dirs); the log
|
|
695
700
|
# streams live, so the link is watchable while the step runs
|
|
696
|
-
step = f"[{s.verb}]({r.
|
|
701
|
+
step = f"[{s.verb}]({r.probe}/{s.log})" if s.log else s.verb
|
|
697
702
|
lines.append(
|
|
698
703
|
f"| {step} | `{s.cmd}` | {code} | {dur} |"
|
|
699
704
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git_bisectlib
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Write tiny git bisect recipes in Python; watch progress live in .bisect/status.md.
|
|
5
5
|
Author-email: Martin Leitner-Ankerl <martin.ankerl@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -152,7 +152,7 @@ Date: 2026-06-15 11:40:00 +0200
|
|
|
152
152
|
1 file changed, 6 insertions(+), 6 deletions(-)
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
| good | bad |
|
|
155
|
+
| good | bad | probe | range | status |
|
|
156
156
|
|------|-----|----------|-------|--------|
|
|
157
157
|
| `2801e9572` …, Bob | `79cb050c2` …, Alice | `cb5394973` …, Carol | 27d 15h · 11 commits | 🟢 good |
|
|
158
158
|
| `cb5394973` …, Carol | `79cb050c2` …, Alice | `95345541b` …, Dan | 12d 7h · 6 commits | 🔴 bad · 81.2s |
|
|
@@ -160,7 +160,7 @@ Date: 2026-06-15 11:40:00 +0200
|
|
|
160
160
|
| `cb5394973` …, Carol | `5c9dcafb3` …, Eve | `19d89b121` …, Fay | 3d 5h · 2 commits | 🟢 good |
|
|
161
161
|
````
|
|
162
162
|
|
|
163
|
-
Each row reads in causal order — the **input range** (`good`/`bad`) → the **
|
|
163
|
+
Each row reads in causal order — the **input range** (`good`/`bad`) → the **probe** git
|
|
164
164
|
chose → the **result** — so you see the range funnel down as you scan. The report is
|
|
165
165
|
re-rendered the moment each command *starts*, links every step to its live-streamed log
|
|
166
166
|
under `.bisect/<sha>/`, and — when the search resolves — shows the culprit the way
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "git_bisectlib"
|
|
7
|
-
version = "0.16.
|
|
7
|
+
version = "0.16.2"
|
|
8
8
|
description = "Write tiny git bisect recipes in Python; watch progress live in .bisect/status.md."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -83,9 +83,9 @@ class TestReport(unittest.TestCase):
|
|
|
83
83
|
rep = _report.build_report(d)
|
|
84
84
|
self.assertIsNotNone(rep)
|
|
85
85
|
self.assertGreaterEqual(len(rep.rows), 1)
|
|
86
|
-
# first row's
|
|
86
|
+
# first row's probe is the first commit git checked out
|
|
87
87
|
first = rep.rows[0]
|
|
88
|
-
self.assertEqual(first.
|
|
88
|
+
self.assertEqual(first.probe, head1)
|
|
89
89
|
self.assertEqual(first.status, "bad" if has_bug else "good")
|
|
90
90
|
# there should be an in-flight todo row for the new HEAD
|
|
91
91
|
self.assertTrue(rep.in_progress)
|
|
@@ -106,7 +106,7 @@ class TestReport(unittest.TestCase):
|
|
|
106
106
|
def test_range_count_excludes_all_goods(self):
|
|
107
107
|
# In a merge DAG, git's candidate range excludes ancestors of EVERY good,
|
|
108
108
|
# not just the latest. With a good anchor on a side branch that diverges
|
|
109
|
-
# from the mainline
|
|
109
|
+
# from the mainline probe, counting only `latest_good..bad` overcounts.
|
|
110
110
|
d = tempfile.mkdtemp(prefix="bisect-report-dag-")
|
|
111
111
|
run(d, "git", "init", "-q")
|
|
112
112
|
run(d, "git", "config", "user.email", "t@t.t")
|
|
@@ -184,8 +184,8 @@ class TestReport(unittest.TestCase):
|
|
|
184
184
|
without_sc = _report.build_report(d) # no sidecar → no invented row
|
|
185
185
|
finally:
|
|
186
186
|
_report.is_ancestor = orig
|
|
187
|
-
self.assertIn(head, [r.
|
|
188
|
-
self.assertNotIn(head, [r.
|
|
187
|
+
self.assertIn(head, [r.probe for r in with_sc.rows])
|
|
188
|
+
self.assertNotIn(head, [r.probe for r in without_sc.rows])
|
|
189
189
|
run(d, "git", "bisect", "reset")
|
|
190
190
|
|
|
191
191
|
def test_cells_show_date_and_author_not_subject(self):
|
|
@@ -256,12 +256,12 @@ class TestReport(unittest.TestCase):
|
|
|
256
256
|
# a finalized sidecar surfaces the real verdict on the in-flight row
|
|
257
257
|
write(pending=False, outcome="bad")
|
|
258
258
|
rep = _report.build_report(d, logs_dir=logs)
|
|
259
|
-
row = next(r for r in rep.rows if r.
|
|
259
|
+
row = next(r for r in rep.rows if r.probe == head)
|
|
260
260
|
self.assertEqual(row.status, "bad")
|
|
261
261
|
# while still pending, it stays `todo`
|
|
262
262
|
write(pending=True, outcome="good")
|
|
263
263
|
rep = _report.build_report(d, logs_dir=logs)
|
|
264
|
-
row = next(r for r in rep.rows if r.
|
|
264
|
+
row = next(r for r in rep.rows if r.probe == head)
|
|
265
265
|
self.assertEqual(row.status, "todo")
|
|
266
266
|
run(d, "git", "bisect", "reset")
|
|
267
267
|
|
|
@@ -280,7 +280,7 @@ class TestReport(unittest.TestCase):
|
|
|
280
280
|
rep = _report.build_report(d)
|
|
281
281
|
finally:
|
|
282
282
|
_report.is_ancestor = orig
|
|
283
|
-
self.assertIn(m1, [r.
|
|
283
|
+
self.assertIn(m1, [r.probe for r in rep.rows])
|
|
284
284
|
run(d, "git", "bisect", "reset")
|
|
285
285
|
|
|
286
286
|
def test_render_format(self):
|
|
@@ -291,13 +291,13 @@ class TestReport(unittest.TestCase):
|
|
|
291
291
|
rep = _report.build_report(d)
|
|
292
292
|
rep.rows[0].sidecar = _report.Sidecar(
|
|
293
293
|
fixups=[{"kind": "replace", "path": "f",
|
|
294
|
-
"
|
|
294
|
+
"old": "OLD_VALUE", "new": "NEW_VALUE"}], steps=[])
|
|
295
295
|
md = _report.render_markdown(rep, details=True)
|
|
296
|
-
self.assertIn("| good | bad |
|
|
296
|
+
self.assertIn("| good | bad | probe | range | status |", md)
|
|
297
297
|
self.assertRegex(md, r"🟢|🔴") # status icons
|
|
298
298
|
self.assertNotIn("✅", md)
|
|
299
299
|
self.assertNotIn("→ ", md.split("## Details")[0]) # no dates/arrows in range
|
|
300
|
-
self.assertIn("`OLD_VALUE → NEW_VALUE`", md)
|
|
300
|
+
self.assertIn("in `f`: `OLD_VALUE` → `NEW_VALUE`", md)
|
|
301
301
|
run(d, "git", "bisect", "reset")
|
|
302
302
|
|
|
303
303
|
def test_no_bisect_returns_none(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|