assertion-cli 0.1.0__py3-none-any.whl

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.py ADDED
@@ -0,0 +1,189 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import NoReturn, Sequence
4
+
5
+ import typer
6
+
7
+
8
+ def exit_with_error(message: str) -> NoReturn:
9
+ typer.echo(message, err=True)
10
+ raise typer.Exit(code=1)
11
+
12
+
13
+ def run_git_command(repo_root: Path, args: Sequence[str]) -> str:
14
+ completed = subprocess.run(
15
+ ["git", *args],
16
+ cwd=repo_root,
17
+ capture_output=True,
18
+ text=True,
19
+ check=False,
20
+ )
21
+ if completed.returncode != 0:
22
+ message = completed.stderr.strip() or completed.stdout.strip() or "git failed"
23
+ raise RuntimeError(message)
24
+ return completed.stdout.strip()
25
+
26
+
27
+ def find_git_root(start_path: Path) -> Path:
28
+ completed = subprocess.run(
29
+ ["git", "rev-parse", "--show-toplevel"],
30
+ cwd=start_path,
31
+ capture_output=True,
32
+ text=True,
33
+ check=False,
34
+ )
35
+ if completed.returncode != 0:
36
+ exit_with_error("Current directory is not inside a git repository.")
37
+ return Path(completed.stdout.strip())
38
+
39
+
40
+ def require_head_pushed(repo_root: Path) -> None:
41
+ try:
42
+ remote_refs = run_git_command(
43
+ repo_root, ["for-each-ref", "--format=%(refname:short)", "refs/remotes"]
44
+ )
45
+ except RuntimeError as exc:
46
+ exit_with_error(f"Failed to inspect remote refs: {exc}")
47
+
48
+ refs = [
49
+ ref for ref in remote_refs.splitlines() if ref and not ref.endswith("/HEAD")
50
+ ]
51
+ if not refs:
52
+ exit_with_error(
53
+ "Current HEAD commit is not present on any remote-tracking branch."
54
+ )
55
+
56
+ for ref in refs:
57
+ completed = subprocess.run(
58
+ ["git", "merge-base", "--is-ancestor", "HEAD", ref],
59
+ cwd=repo_root,
60
+ capture_output=True,
61
+ text=True,
62
+ check=False,
63
+ )
64
+ if completed.returncode == 0:
65
+ return
66
+ if completed.returncode != 1:
67
+ message = (
68
+ completed.stderr.strip() or completed.stdout.strip() or "git failed"
69
+ )
70
+ exit_with_error(f"Failed to verify remote commit state: {message}")
71
+
72
+ exit_with_error("Current HEAD commit is not present on any remote-tracking branch.")
73
+
74
+
75
+ def get_head_sha(repo_root: Path) -> str:
76
+ try:
77
+ return run_git_command(repo_root, ["rev-parse", "HEAD"])
78
+ except RuntimeError as exc:
79
+ exit_with_error(f"Failed to get HEAD SHA: {exc}")
80
+
81
+
82
+ def get_head_branch(repo_root: Path) -> str | None:
83
+ """Return the current branch name, or None if HEAD is detached."""
84
+ try:
85
+ name = run_git_command(repo_root, ["rev-parse", "--abbrev-ref", "HEAD"])
86
+ except RuntimeError:
87
+ return None
88
+ return name if name and name != "HEAD" else None
89
+
90
+
91
+ def get_origin_github_repo(repo_root: Path) -> str | None:
92
+ """Return the current repo's GitHub `owner/name` from origin, or None if not parseable.
93
+
94
+ Accepts the common remote URL forms:
95
+ git@github.com:owner/name(.git)?
96
+ https://github.com/owner/name(.git)?
97
+ ssh://git@github.com/owner/name(.git)?
98
+ """
99
+ try:
100
+ url = run_git_command(repo_root, ["remote", "get-url", "origin"]).strip()
101
+ except RuntimeError:
102
+ return None
103
+ if not url:
104
+ return None
105
+
106
+ if url.startswith("git@github.com:"):
107
+ path = url[len("git@github.com:") :]
108
+ elif url.startswith("ssh://git@github.com/"):
109
+ path = url[len("ssh://git@github.com/") :]
110
+ elif url.startswith("https://github.com/"):
111
+ path = url[len("https://github.com/") :]
112
+ elif url.startswith("http://github.com/"):
113
+ path = url[len("http://github.com/") :]
114
+ else:
115
+ return None
116
+
117
+ path = path.rstrip("/")
118
+ if path.endswith(".git"):
119
+ path = path[: -len(".git")]
120
+ if path.count("/") != 1 or not all(path.split("/")):
121
+ return None
122
+ return path
123
+
124
+
125
+ def _build_untracked_diff(repo_root: Path, rel_path: str) -> str:
126
+ completed = subprocess.run(
127
+ [
128
+ "git",
129
+ "diff",
130
+ "--no-index",
131
+ "--src-prefix=a/",
132
+ "--dst-prefix=b/",
133
+ "--",
134
+ "/dev/null",
135
+ rel_path,
136
+ ],
137
+ cwd=repo_root,
138
+ capture_output=True,
139
+ text=True,
140
+ check=False,
141
+ )
142
+ if completed.returncode not in (0, 1):
143
+ message = completed.stderr.strip() or completed.stdout.strip() or "git failed"
144
+ raise RuntimeError(message)
145
+ return completed.stdout.strip()
146
+
147
+
148
+ # Paths the assertion-cli owns end-to-end. Excluded from the diff bundle so
149
+ # reviewers don't flag our own bootstrap files as "unrelated changes" — they're
150
+ # generated/refreshed by `asrt init` and `asrt checkpoint` and have nothing to
151
+ # do with the customer's feature work. CLAUDE.md / AGENTS.md are intentionally
152
+ # NOT in this list: those are customer-owned files we only patch a marked
153
+ # block into, so the customer's other edits to them should still flow through.
154
+ _ASSERTION_EXCLUDED_PATHSPECS = [
155
+ ":(exclude).assertion",
156
+ ":(exclude).claude/skills/assertion-cli",
157
+ ":(exclude).agents/skills/assertion-cli",
158
+ ]
159
+
160
+
161
+ def get_uncommitted_diff(repo_root: Path) -> str:
162
+ try:
163
+ tracked = run_git_command(
164
+ repo_root, ["diff", "--", *_ASSERTION_EXCLUDED_PATHSPECS]
165
+ )
166
+ staged = run_git_command(
167
+ repo_root, ["diff", "--cached", "--", *_ASSERTION_EXCLUDED_PATHSPECS]
168
+ )
169
+
170
+ untracked_output = run_git_command(
171
+ repo_root,
172
+ [
173
+ "ls-files",
174
+ "--others",
175
+ "--exclude-standard",
176
+ "--",
177
+ *_ASSERTION_EXCLUDED_PATHSPECS,
178
+ ],
179
+ )
180
+ untracked_diffs = [
181
+ _build_untracked_diff(repo_root, rel_path)
182
+ for rel_path in untracked_output.splitlines()
183
+ if rel_path
184
+ ]
185
+
186
+ parts = [p for p in [tracked, staged, "\n".join(untracked_diffs)] if p]
187
+ return "\n".join(parts)
188
+ except RuntimeError as exc:
189
+ exit_with_error(f"Failed to collect git diff: {exc}")
link.py ADDED
@@ -0,0 +1,21 @@
1
+ from pathlib import Path
2
+
3
+ from git import exit_with_error
4
+
5
+ LINK_FILE_NAME = "link"
6
+
7
+
8
+ def save_link(assertion_dir: Path, url: str) -> None:
9
+ (assertion_dir / LINK_FILE_NAME).write_text(url + "\n", encoding="utf-8")
10
+
11
+
12
+ def load_link(assertion_dir: Path) -> str:
13
+ link_path = assertion_dir / LINK_FILE_NAME
14
+ if not link_path.exists():
15
+ exit_with_error(
16
+ "No session link found. Run `asrt verify` first to generate a link."
17
+ )
18
+ content = link_path.read_text(encoding="utf-8").strip()
19
+ if not content:
20
+ exit_with_error("Session link file is empty. Run `asrt verify` to regenerate.")
21
+ return content