assertion-cli 0.5.9__tar.gz → 0.5.10__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.
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/PKG-INFO +1 -1
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/assertion_cli.egg-info/PKG-INFO +1 -1
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/git.py +52 -4
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/main.py +11 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/pyproject.toml +1 -1
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_git.py +181 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/README.md +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/api.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/assertion_cli.egg-info/SOURCES.txt +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/assertion_cli.egg-info/dependency_links.txt +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/assertion_cli.egg-info/entry_points.txt +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/assertion_cli.egg-info/requires.txt +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/assertion_cli.egg-info/top_level.txt +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/bundle.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/link.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/models.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/session.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/setup.cfg +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/templates/AGENTS.md +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/templates/SKILL.md +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/templates/__init__.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_api.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_bundle.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_decision.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_install.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_link.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_main.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_new.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_prompt.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_session.py +0 -0
- {assertion_cli-0.5.9 → assertion_cli-0.5.10}/tests/test_session_start.py +0 -0
|
@@ -49,11 +49,14 @@ def get_head_sha(repo_root: Path) -> str:
|
|
|
49
49
|
def get_upstream_ref(repo_root: Path) -> str | None:
|
|
50
50
|
"""Return the upstream tracking ref (e.g. ``origin/main``).
|
|
51
51
|
|
|
52
|
-
Tries HEAD's configured upstream first
|
|
53
|
-
|
|
54
|
-
remote
|
|
55
|
-
|
|
52
|
+
Tries HEAD's configured upstream first, then falls through increasingly
|
|
53
|
+
general heuristics to find a remote ref to compute a merge-base against:
|
|
54
|
+
same-named remote branch, closest remote ancestor (via
|
|
55
|
+
``git branch -r --merged HEAD`` + ``git merge-base``), then well-known
|
|
56
|
+
default branch names. Returns ``None`` when none are available.
|
|
56
57
|
"""
|
|
58
|
+
|
|
59
|
+
# 1. Configured upstream (git branch -u).
|
|
57
60
|
try:
|
|
58
61
|
return run_git_command(
|
|
59
62
|
repo_root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]
|
|
@@ -61,6 +64,7 @@ def get_upstream_ref(repo_root: Path) -> str | None:
|
|
|
61
64
|
except RuntimeError:
|
|
62
65
|
pass
|
|
63
66
|
|
|
67
|
+
# 2. Same branch name on remote (has been pushed at least once).
|
|
64
68
|
head_branch = get_head_branch(repo_root)
|
|
65
69
|
if head_branch is not None:
|
|
66
70
|
fallback = f"origin/{head_branch}"
|
|
@@ -71,6 +75,50 @@ def get_upstream_ref(repo_root: Path) -> str | None:
|
|
|
71
75
|
else:
|
|
72
76
|
return fallback
|
|
73
77
|
|
|
78
|
+
# 3. Closest remote ancestor — handles branches based on other pushed
|
|
79
|
+
# branches (e.g. my-feature on my-feature-base). Uses --merged HEAD
|
|
80
|
+
# so only remote branches in HEAD's ancestry are considered; a branch
|
|
81
|
+
# whose tip moved forward after we branched off is correctly excluded.
|
|
82
|
+
try:
|
|
83
|
+
merged = run_git_command(
|
|
84
|
+
repo_root,
|
|
85
|
+
[
|
|
86
|
+
"branch", "-r", "--merged", "HEAD",
|
|
87
|
+
"--format=%(refname:short)",
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
ancestors = [r for r in merged.splitlines() if r]
|
|
91
|
+
if ancestors:
|
|
92
|
+
return run_git_command(
|
|
93
|
+
repo_root, ["merge-base", "HEAD", *ancestors]
|
|
94
|
+
)
|
|
95
|
+
except RuntimeError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
# 4. Remote's default branch (symbolic ref, usually origin/main).
|
|
99
|
+
try:
|
|
100
|
+
run_git_command(repo_root, ["rev-parse", "--verify", "origin/HEAD"])
|
|
101
|
+
except RuntimeError:
|
|
102
|
+
pass
|
|
103
|
+
else:
|
|
104
|
+
return "origin/HEAD"
|
|
105
|
+
|
|
106
|
+
# 5. Conventional default branch name.
|
|
107
|
+
try:
|
|
108
|
+
run_git_command(repo_root, ["rev-parse", "--verify", "origin/main"])
|
|
109
|
+
except RuntimeError:
|
|
110
|
+
pass
|
|
111
|
+
else:
|
|
112
|
+
return "origin/main"
|
|
113
|
+
|
|
114
|
+
# 6. Legacy default branch name.
|
|
115
|
+
try:
|
|
116
|
+
run_git_command(repo_root, ["rev-parse", "--verify", "origin/master"])
|
|
117
|
+
except RuntimeError:
|
|
118
|
+
pass
|
|
119
|
+
else:
|
|
120
|
+
return "origin/master"
|
|
121
|
+
|
|
74
122
|
return None
|
|
75
123
|
|
|
76
124
|
|
|
@@ -19,6 +19,7 @@ from git import (
|
|
|
19
19
|
get_merge_base,
|
|
20
20
|
get_uncommitted_diff,
|
|
21
21
|
get_upstream_ref,
|
|
22
|
+
run_git_command,
|
|
22
23
|
)
|
|
23
24
|
from link import load_link, save_link
|
|
24
25
|
from models import CheckpointResponse, SessionStatus, render_stack_list
|
|
@@ -265,9 +266,19 @@ app.add_typer(session_app, name="session")
|
|
|
265
266
|
def _resolve_diff_base(repo_root: Path) -> str:
|
|
266
267
|
"""Compute the merge-base with the upstream tracking branch.
|
|
267
268
|
|
|
269
|
+
Fetches from origin first so the merge-base SHA is guaranteed to exist
|
|
270
|
+
on the remote when the backend checks it out — stale remote tracking
|
|
271
|
+
refs (from a force-pushed or rebased branch) would otherwise produce a
|
|
272
|
+
merge-base that's no longer reachable from any remote ref.
|
|
273
|
+
|
|
268
274
|
Errors out when no upstream can be resolved (detached HEAD, no tracking
|
|
269
275
|
branch, branch not pushed to remote).
|
|
270
276
|
"""
|
|
277
|
+
try:
|
|
278
|
+
run_git_command(repo_root, ["fetch", "--no-tags", "origin"])
|
|
279
|
+
except RuntimeError:
|
|
280
|
+
pass # non-fatal: offline or unavailable; proceed with stale refs
|
|
281
|
+
|
|
271
282
|
upstream = get_upstream_ref(repo_root)
|
|
272
283
|
if upstream is None:
|
|
273
284
|
exit_with_error(
|
|
@@ -180,6 +180,187 @@ def test_get_upstream_ref_detached_head(tmp_path: Path) -> None:
|
|
|
180
180
|
assert ref is None
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
def test_get_upstream_ref_fallback_to_origin_head(tmp_path: Path) -> None:
|
|
184
|
+
"""HEAD on an orphan commit (no shared history with remote refs)
|
|
185
|
+
returns origin/HEAD."""
|
|
186
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
187
|
+
_make_commit(tmp_path)
|
|
188
|
+
subprocess.run(
|
|
189
|
+
["git", "remote", "add", "origin", "http://example.com/repo.git"],
|
|
190
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
191
|
+
)
|
|
192
|
+
subprocess.run(
|
|
193
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/somebranch"],
|
|
194
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
195
|
+
)
|
|
196
|
+
subprocess.run(
|
|
197
|
+
["git", "update-ref", "refs/remotes/origin/somebranch", "HEAD"],
|
|
198
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
199
|
+
)
|
|
200
|
+
# Create an orphan commit so HEAD has no shared history with the
|
|
201
|
+
# remote refs — this makes step 3 (--merged HEAD) produce nothing.
|
|
202
|
+
subprocess.run(
|
|
203
|
+
["git", "checkout", "--orphan", "orphan-branch"],
|
|
204
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
205
|
+
)
|
|
206
|
+
(tmp_path / "orphan.txt").write_text("orphan\n")
|
|
207
|
+
subprocess.run(
|
|
208
|
+
["git", "add", "orphan.txt"], cwd=tmp_path, capture_output=True, check=True
|
|
209
|
+
)
|
|
210
|
+
subprocess.run(
|
|
211
|
+
["git", "commit", "-m", "orphan"],
|
|
212
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
213
|
+
env={**GIT_ENV, "HOME": str(tmp_path)},
|
|
214
|
+
)
|
|
215
|
+
ref = get_upstream_ref(tmp_path)
|
|
216
|
+
assert ref == "origin/HEAD"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_get_upstream_ref_fallback_to_origin_main(tmp_path: Path) -> None:
|
|
220
|
+
"""HEAD on an orphan commit, no origin/HEAD, but origin/main exists."""
|
|
221
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
222
|
+
_make_commit(tmp_path)
|
|
223
|
+
subprocess.run(
|
|
224
|
+
["git", "remote", "add", "origin", "http://example.com/repo.git"],
|
|
225
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
226
|
+
)
|
|
227
|
+
subprocess.run(
|
|
228
|
+
["git", "update-ref", "refs/remotes/origin/main", "HEAD"],
|
|
229
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
230
|
+
)
|
|
231
|
+
subprocess.run(
|
|
232
|
+
["git", "checkout", "--orphan", "orphan-branch"],
|
|
233
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
234
|
+
)
|
|
235
|
+
(tmp_path / "orphan.txt").write_text("orphan\n")
|
|
236
|
+
subprocess.run(
|
|
237
|
+
["git", "add", "orphan.txt"], cwd=tmp_path, capture_output=True, check=True
|
|
238
|
+
)
|
|
239
|
+
subprocess.run(
|
|
240
|
+
["git", "commit", "-m", "orphan"],
|
|
241
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
242
|
+
env={**GIT_ENV, "HOME": str(tmp_path)},
|
|
243
|
+
)
|
|
244
|
+
ref = get_upstream_ref(tmp_path)
|
|
245
|
+
assert ref == "origin/main"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_get_upstream_ref_fallback_to_origin_master(tmp_path: Path) -> None:
|
|
249
|
+
"""HEAD on an orphan commit, no origin/HEAD/main, origin/master exists."""
|
|
250
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
251
|
+
_make_commit(tmp_path)
|
|
252
|
+
subprocess.run(
|
|
253
|
+
["git", "remote", "add", "origin", "http://example.com/repo.git"],
|
|
254
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
255
|
+
)
|
|
256
|
+
subprocess.run(
|
|
257
|
+
["git", "update-ref", "refs/remotes/origin/master", "HEAD"],
|
|
258
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
259
|
+
)
|
|
260
|
+
subprocess.run(
|
|
261
|
+
["git", "checkout", "--orphan", "orphan-branch"],
|
|
262
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
263
|
+
)
|
|
264
|
+
(tmp_path / "orphan.txt").write_text("orphan\n")
|
|
265
|
+
subprocess.run(
|
|
266
|
+
["git", "add", "orphan.txt"], cwd=tmp_path, capture_output=True, check=True
|
|
267
|
+
)
|
|
268
|
+
subprocess.run(
|
|
269
|
+
["git", "commit", "-m", "orphan"],
|
|
270
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
271
|
+
env={**GIT_ENV, "HOME": str(tmp_path)},
|
|
272
|
+
)
|
|
273
|
+
ref = get_upstream_ref(tmp_path)
|
|
274
|
+
assert ref == "origin/master"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_get_upstream_ref_fallback_to_merged_ancestors(tmp_path: Path) -> None:
|
|
278
|
+
"""Branch based on a pushed feature branch finds the closest remote
|
|
279
|
+
ancestor via --merged HEAD + merge-base."""
|
|
280
|
+
_init_repo_with_commit(tmp_path)
|
|
281
|
+
base_sha = run_git_command(tmp_path, ["rev-parse", "HEAD"])
|
|
282
|
+
subprocess.run(
|
|
283
|
+
["git", "remote", "add", "origin", "http://example.com/repo.git"],
|
|
284
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Simulate origin/main at the initial commit.
|
|
288
|
+
subprocess.run(
|
|
289
|
+
["git", "update-ref", "refs/remotes/origin/main", base_sha],
|
|
290
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Simulate a pushed feature branch (origin/my-feature-base) with a
|
|
294
|
+
# commit on top of main.
|
|
295
|
+
(tmp_path / "base.txt").write_text("base\n")
|
|
296
|
+
subprocess.run(
|
|
297
|
+
["git", "add", "base.txt"], cwd=tmp_path, capture_output=True, check=True
|
|
298
|
+
)
|
|
299
|
+
subprocess.run(
|
|
300
|
+
["git", "commit", "-m", "base work"],
|
|
301
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
302
|
+
env={**GIT_ENV, "HOME": str(tmp_path)},
|
|
303
|
+
)
|
|
304
|
+
feature_base_sha = run_git_command(tmp_path, ["rev-parse", "HEAD"])
|
|
305
|
+
subprocess.run(
|
|
306
|
+
["git", "update-ref", "refs/remotes/origin/my-feature-base", feature_base_sha],
|
|
307
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Create a new local branch on top of my-feature-base (simulates a
|
|
311
|
+
# branch-off that hasn't been pushed). Use a branch name that doesn't
|
|
312
|
+
# collide with any remote ref so step 2 is skipped.
|
|
313
|
+
subprocess.run(
|
|
314
|
+
["git", "checkout", "-b", "my-feature"],
|
|
315
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
316
|
+
)
|
|
317
|
+
(tmp_path / "new.txt").write_text("new\n")
|
|
318
|
+
subprocess.run(
|
|
319
|
+
["git", "add", "new.txt"], cwd=tmp_path, capture_output=True, check=True
|
|
320
|
+
)
|
|
321
|
+
subprocess.run(
|
|
322
|
+
["git", "commit", "-m", "new work"],
|
|
323
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
324
|
+
env={**GIT_ENV, "HOME": str(tmp_path)},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
ref = get_upstream_ref(tmp_path)
|
|
328
|
+
assert ref is not None
|
|
329
|
+
# ref was returned by `git merge-base HEAD ...`, it should be the
|
|
330
|
+
# SHA of the feature base, not a named ref.
|
|
331
|
+
assert ref == feature_base_sha
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_get_upstream_ref_no_fallback(tmp_path: Path) -> None:
|
|
335
|
+
"""No upstream, no origin/HEAD, no known default branches returns None."""
|
|
336
|
+
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
|
|
337
|
+
_make_commit(tmp_path)
|
|
338
|
+
subprocess.run(
|
|
339
|
+
["git", "remote", "add", "origin", "http://example.com/repo.git"],
|
|
340
|
+
cwd=tmp_path, capture_output=True, check=True,
|
|
341
|
+
)
|
|
342
|
+
ref = get_upstream_ref(tmp_path)
|
|
343
|
+
assert ref is None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _make_commit(tmp_path: Path) -> None:
|
|
347
|
+
"""Helper: make an initial commit in a bare init."""
|
|
348
|
+
subprocess.run(
|
|
349
|
+
["git", "commit", "--allow-empty", "-m", "init"],
|
|
350
|
+
cwd=tmp_path,
|
|
351
|
+
capture_output=True,
|
|
352
|
+
check=True,
|
|
353
|
+
env={
|
|
354
|
+
"GIT_AUTHOR_NAME": "T",
|
|
355
|
+
"GIT_AUTHOR_EMAIL": "t@t",
|
|
356
|
+
"GIT_COMMITTER_NAME": "T",
|
|
357
|
+
"GIT_COMMITTER_EMAIL": "t@t",
|
|
358
|
+
"HOME": str(tmp_path),
|
|
359
|
+
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin",
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
183
364
|
# --- get_merge_base ---
|
|
184
365
|
|
|
185
366
|
|
|
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
|