arctx-cli 0.2.0b2__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.
- arctx_cli/__init__.py +1 -0
- arctx_cli/alias.py +238 -0
- arctx_cli/append_batch.py +90 -0
- arctx_cli/commands/__init__.py +85 -0
- arctx_cli/commands/alias_cmd.py +174 -0
- arctx_cli/commands/anchor.py +82 -0
- arctx_cli/commands/current.py +69 -0
- arctx_cli/commands/cut.py +89 -0
- arctx_cli/commands/dump.py +72 -0
- arctx_cli/commands/ext.py +236 -0
- arctx_cli/commands/git.py +216 -0
- arctx_cli/commands/graph.py +73 -0
- arctx_cli/commands/guide.py +360 -0
- arctx_cli/commands/init.py +223 -0
- arctx_cli/commands/list.py +45 -0
- arctx_cli/commands/migrate.py +135 -0
- arctx_cli/commands/node.py +55 -0
- arctx_cli/commands/outcomes.py +58 -0
- arctx_cli/commands/payload.py +192 -0
- arctx_cli/commands/reachable.py +75 -0
- arctx_cli/commands/show.py +113 -0
- arctx_cli/commands/sync.py +244 -0
- arctx_cli/commands/trace.py +46 -0
- arctx_cli/commands/transition.py +212 -0
- arctx_cli/commands/use.py +67 -0
- arctx_cli/commands/view.py +82 -0
- arctx_cli/commands/work_session.py +330 -0
- arctx_cli/context.py +38 -0
- arctx_cli/ext/__init__.py +1 -0
- arctx_cli/ext/command/__init__.py +110 -0
- arctx_cli/ext/git/__init__.py +1 -0
- arctx_cli/ext/git/branch.py +140 -0
- arctx_cli/ext/git/cherry_pick.py +144 -0
- arctx_cli/ext/git/commit.py +205 -0
- arctx_cli/ext/git/hook.py +758 -0
- arctx_cli/ext/git/merge.py +204 -0
- arctx_cli/ext/git/reset.py +138 -0
- arctx_cli/ext/git/revert.py +157 -0
- arctx_cli/ext/git/verify.py +140 -0
- arctx_cli/ext/git/worktree.py +173 -0
- arctx_cli/ext_registry.py +34 -0
- arctx_cli/main.py +133 -0
- arctx_cli/paths.py +27 -0
- arctx_cli/payload_builder.py +23 -0
- arctx_cli/workspace.py +64 -0
- arctx_cli-0.2.0b2.dist-info/METADATA +48 -0
- arctx_cli-0.2.0b2.dist-info/RECORD +49 -0
- arctx_cli-0.2.0b2.dist-info/WHEEL +4 -0
- arctx_cli-0.2.0b2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""arctx CLI merge command.
|
|
2
|
+
|
|
3
|
+
Drives a git merge (or arctx-only join) and records a multi-input Transition
|
|
4
|
+
with MergePayload or JoinPayload.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
arctx merge --other <ref> [-m <message>] [--join]
|
|
8
|
+
arctx merge --other branch:<name> [--join]
|
|
9
|
+
arctx merge --other node:<id> [--join]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from arctx_cli.append_batch import graph_counts, maybe_append_or_save
|
|
19
|
+
from arctx_cli.context import (
|
|
20
|
+
resolve_run_id_from_args,
|
|
21
|
+
resolve_store,
|
|
22
|
+
resolve_user_id_from_args,
|
|
23
|
+
resolve_work_session_id_from_args,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_merge_ref(ref: str) -> tuple[str | None, str | None]:
|
|
28
|
+
"""Parse a merge ref into (other_branch, other_node_id).
|
|
29
|
+
|
|
30
|
+
Formats accepted:
|
|
31
|
+
- "branch:<name>" → other_branch
|
|
32
|
+
- "node:<id>" → other_node_id
|
|
33
|
+
- "<anything>" → treated as branch name
|
|
34
|
+
"""
|
|
35
|
+
if ref.startswith("branch:"):
|
|
36
|
+
return ref[len("branch:"):], None
|
|
37
|
+
if ref.startswith("node:"):
|
|
38
|
+
return None, ref[len("node:"):]
|
|
39
|
+
return ref, None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
43
|
+
"""Register the ``merge`` subcommand parser."""
|
|
44
|
+
p = subparsers.add_parser(
|
|
45
|
+
"merge",
|
|
46
|
+
help="Drive a git merge and record a multi-input arctx transition",
|
|
47
|
+
)
|
|
48
|
+
p.add_argument(
|
|
49
|
+
"--other",
|
|
50
|
+
required=True,
|
|
51
|
+
metavar="REF",
|
|
52
|
+
help=(
|
|
53
|
+
"The branch or node to merge in. Format: 'branch:<name>', "
|
|
54
|
+
"'node:<id>', or just '<name>' (auto-detected as branch name)."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
p.add_argument(
|
|
58
|
+
"-m",
|
|
59
|
+
"--message",
|
|
60
|
+
default=None,
|
|
61
|
+
help="Override the merge commit message",
|
|
62
|
+
)
|
|
63
|
+
p.add_argument(
|
|
64
|
+
"--branch",
|
|
65
|
+
default=None,
|
|
66
|
+
help="Override the current branch name (default: inferred from git)",
|
|
67
|
+
)
|
|
68
|
+
p.add_argument(
|
|
69
|
+
"--join",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help=(
|
|
72
|
+
"Treat as a arctx-only join (no common ancestor). "
|
|
73
|
+
"Records JoinPayload instead of MergePayload. "
|
|
74
|
+
"Does NOT run git merge."
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
p.add_argument("--run", default=None, help="Explicit run id")
|
|
78
|
+
p.add_argument("--store-dir", default=None, help="Store directory")
|
|
79
|
+
p.add_argument("--user", default=None, help="User id for attribution")
|
|
80
|
+
p.add_argument("--work-session", default=None, help="Work session id")
|
|
81
|
+
return p
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run_merge_command(
|
|
85
|
+
*,
|
|
86
|
+
other: str,
|
|
87
|
+
message: str | None,
|
|
88
|
+
branch: str | None,
|
|
89
|
+
run_id: str | None,
|
|
90
|
+
store_dir: str | None,
|
|
91
|
+
user_id: str | None,
|
|
92
|
+
work_session_id: str | None,
|
|
93
|
+
join: bool = False,
|
|
94
|
+
# Test-only parameters; not exposed in the CLI parser.
|
|
95
|
+
dry_run: bool = False,
|
|
96
|
+
head_commit: str | None = None,
|
|
97
|
+
) -> dict:
|
|
98
|
+
"""Execute a merge (or join) and persist the resulting graph records.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
other:
|
|
103
|
+
Merge target reference. Format: 'branch:<name>', 'node:<id>', or '<name>'.
|
|
104
|
+
message:
|
|
105
|
+
Override the merge commit message. If None, git uses its default.
|
|
106
|
+
branch:
|
|
107
|
+
Current branch name override (None → infer from git).
|
|
108
|
+
run_id:
|
|
109
|
+
Explicit run id. If None, resolved from env / <gitdir>/arctx-id.
|
|
110
|
+
store_dir:
|
|
111
|
+
Store directory. If None, resolved from ARCTX_HOME.
|
|
112
|
+
user_id:
|
|
113
|
+
User id for work event attribution.
|
|
114
|
+
work_session_id:
|
|
115
|
+
Work session id.
|
|
116
|
+
join:
|
|
117
|
+
If True, use JoinPayload instead of MergePayload.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict with transition_id, output_node_id, branch, head_commit,
|
|
122
|
+
input_node_ids, merge_payload_type.
|
|
123
|
+
"""
|
|
124
|
+
other_branch, other_node_id = _parse_merge_ref(other)
|
|
125
|
+
|
|
126
|
+
store = resolve_store(store_dir)
|
|
127
|
+
handle = store.load_run(run_id)
|
|
128
|
+
|
|
129
|
+
before = graph_counts(handle)
|
|
130
|
+
|
|
131
|
+
transition = handle.git.merge(
|
|
132
|
+
other_branch=other_branch,
|
|
133
|
+
other_node_id=other_node_id,
|
|
134
|
+
message=message,
|
|
135
|
+
branch=branch,
|
|
136
|
+
user_id=user_id,
|
|
137
|
+
work_session_id=work_session_id,
|
|
138
|
+
join=join,
|
|
139
|
+
dry_run=dry_run,
|
|
140
|
+
head_commit=head_commit,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
maybe_append_or_save(
|
|
144
|
+
store=store,
|
|
145
|
+
handle=handle,
|
|
146
|
+
user_id=user_id,
|
|
147
|
+
work_session_id=work_session_id,
|
|
148
|
+
before=before,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
git_payloads = handle.run_graph.payloads_for_transition(
|
|
152
|
+
transition.transition_id, payload_type="git_change"
|
|
153
|
+
)
|
|
154
|
+
head_commit = git_payloads[-1].head_commit if git_payloads else ""
|
|
155
|
+
branch_payloads = handle.run_graph.payloads_for_transition(
|
|
156
|
+
transition.transition_id, payload_type="branch"
|
|
157
|
+
)
|
|
158
|
+
resolved_branch = branch_payloads[-1].branch if branch_payloads else ""
|
|
159
|
+
|
|
160
|
+
payload_type = "join" if join else "merge"
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"transition_id": transition.transition_id,
|
|
164
|
+
"output_node_id": transition.output_node_id,
|
|
165
|
+
"input_node_ids": list(transition.input_node_ids),
|
|
166
|
+
"branch": resolved_branch,
|
|
167
|
+
"head_commit": head_commit,
|
|
168
|
+
"merge_payload_type": payload_type,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cli_merge(args) -> int:
|
|
173
|
+
"""Entry point for ``arctx merge`` subcommand."""
|
|
174
|
+
from arctx.ext.git.verbs._forward_transition import ParallelSessionConflict # noqa: PLC0415
|
|
175
|
+
|
|
176
|
+
run_id = resolve_run_id_from_args(args)
|
|
177
|
+
user_id = resolve_user_id_from_args(args)
|
|
178
|
+
work_session_id = resolve_work_session_id_from_args(args)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
result = run_merge_command(
|
|
182
|
+
other=args.other,
|
|
183
|
+
message=args.message,
|
|
184
|
+
branch=args.branch,
|
|
185
|
+
run_id=run_id,
|
|
186
|
+
store_dir=args.store_dir,
|
|
187
|
+
user_id=user_id,
|
|
188
|
+
work_session_id=work_session_id,
|
|
189
|
+
join=args.join,
|
|
190
|
+
)
|
|
191
|
+
except ParallelSessionConflict as exc:
|
|
192
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
193
|
+
print(
|
|
194
|
+
"hint: another session has advanced this branch. "
|
|
195
|
+
"Rebase / pull before committing.",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
return 2
|
|
199
|
+
except Exception as exc: # noqa: BLE001
|
|
200
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
201
|
+
return 1
|
|
202
|
+
|
|
203
|
+
print(json.dumps(result, indent=2))
|
|
204
|
+
return 0
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""arctx CLI reset command.
|
|
2
|
+
|
|
3
|
+
Resets the session pointer to a past node WITHOUT creating a new Transition.
|
|
4
|
+
Analogous to ``git reset``: moves HEAD back but does not produce a new commit.
|
|
5
|
+
|
|
6
|
+
For mode="hard", discarded transitions receive a CutPayload. For "mixed" and
|
|
7
|
+
"soft" the transitions are left active (working tree / index changes remain).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from arctx_cli.append_batch import graph_counts, maybe_append_or_save
|
|
17
|
+
from arctx_cli.context import (
|
|
18
|
+
resolve_run_id_from_args,
|
|
19
|
+
resolve_store,
|
|
20
|
+
resolve_user_id_from_args,
|
|
21
|
+
resolve_work_session_id_from_args,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
26
|
+
"""Register the ``reset`` subcommand parser."""
|
|
27
|
+
p = subparsers.add_parser(
|
|
28
|
+
"reset",
|
|
29
|
+
help="Reset to a past node (no new transition)",
|
|
30
|
+
)
|
|
31
|
+
g = p.add_mutually_exclusive_group(required=True)
|
|
32
|
+
g.add_argument("--node", default=None, help="Target node id")
|
|
33
|
+
g.add_argument(
|
|
34
|
+
"--sha", default=None, help="Target commit sha (lookup via transition_by_sha)"
|
|
35
|
+
)
|
|
36
|
+
p.add_argument(
|
|
37
|
+
"--mode",
|
|
38
|
+
choices=["hard", "mixed", "soft"],
|
|
39
|
+
default="hard",
|
|
40
|
+
help="Reset mode (default: hard)",
|
|
41
|
+
)
|
|
42
|
+
p.add_argument("--branch", default=None, help="Override branch name")
|
|
43
|
+
p.add_argument("--run", default=None, help="Explicit run id")
|
|
44
|
+
p.add_argument("--store-dir", default=None, help="Store directory")
|
|
45
|
+
p.add_argument("--user", default=None, help="User id for attribution")
|
|
46
|
+
p.add_argument("--work-session", default=None, help="Work session id")
|
|
47
|
+
return p
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_reset_command(
|
|
51
|
+
*,
|
|
52
|
+
to_node_id: str | None,
|
|
53
|
+
to_sha: str | None,
|
|
54
|
+
mode: str,
|
|
55
|
+
branch: str | None,
|
|
56
|
+
run_id: str | None,
|
|
57
|
+
store_dir: str | None,
|
|
58
|
+
user_id: str | None,
|
|
59
|
+
work_session_id: str | None,
|
|
60
|
+
dry_run: bool = False,
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""Execute a reset and persist the resulting graph records.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
to_node_id:
|
|
67
|
+
Target node id. Mutually exclusive with to_sha.
|
|
68
|
+
to_sha:
|
|
69
|
+
Target commit sha (looked up via transition_by_sha).
|
|
70
|
+
mode:
|
|
71
|
+
"hard" | "mixed" | "soft".
|
|
72
|
+
branch:
|
|
73
|
+
Branch name override for the SessionPointerEvent.
|
|
74
|
+
run_id:
|
|
75
|
+
Explicit run id.
|
|
76
|
+
store_dir:
|
|
77
|
+
Store directory.
|
|
78
|
+
user_id:
|
|
79
|
+
User id for work event attribution.
|
|
80
|
+
work_session_id:
|
|
81
|
+
Work session id.
|
|
82
|
+
dry_run:
|
|
83
|
+
If True, skip actual git operations.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
dict with to_node_id, from_node_id, discarded_transition_ids, mode,
|
|
88
|
+
event_id.
|
|
89
|
+
"""
|
|
90
|
+
store = resolve_store(store_dir)
|
|
91
|
+
handle = store.load_run(run_id)
|
|
92
|
+
|
|
93
|
+
before = graph_counts(handle)
|
|
94
|
+
|
|
95
|
+
result = handle.git.reset(
|
|
96
|
+
to_node_id=to_node_id,
|
|
97
|
+
to_sha=to_sha,
|
|
98
|
+
mode=mode,
|
|
99
|
+
branch=branch,
|
|
100
|
+
user_id=user_id,
|
|
101
|
+
work_session_id=work_session_id,
|
|
102
|
+
dry_run=dry_run,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
maybe_append_or_save(
|
|
106
|
+
store=store,
|
|
107
|
+
handle=handle,
|
|
108
|
+
user_id=user_id,
|
|
109
|
+
work_session_id=work_session_id,
|
|
110
|
+
before=before,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cli_reset(args) -> int:
|
|
117
|
+
"""Entry point for ``arctx reset`` subcommand."""
|
|
118
|
+
run_id = resolve_run_id_from_args(args)
|
|
119
|
+
user_id = resolve_user_id_from_args(args)
|
|
120
|
+
work_session_id = resolve_work_session_id_from_args(args)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
result = run_reset_command(
|
|
124
|
+
to_node_id=args.node,
|
|
125
|
+
to_sha=args.sha,
|
|
126
|
+
mode=args.mode,
|
|
127
|
+
branch=args.branch,
|
|
128
|
+
run_id=run_id,
|
|
129
|
+
store_dir=args.store_dir,
|
|
130
|
+
user_id=user_id,
|
|
131
|
+
work_session_id=work_session_id,
|
|
132
|
+
)
|
|
133
|
+
except Exception as exc: # noqa: BLE001
|
|
134
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
print(json.dumps(result, indent=2))
|
|
138
|
+
return 0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""arctx CLI revert command.
|
|
2
|
+
|
|
3
|
+
Drives a ``git revert`` and records the corresponding arctx Transition with
|
|
4
|
+
BranchPayload, GitChangePayload, RevertPayload, BranchTipEvent, and
|
|
5
|
+
SessionPointerEvent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from arctx_cli.append_batch import graph_counts, maybe_append_or_save
|
|
15
|
+
from arctx_cli.context import (
|
|
16
|
+
resolve_run_id_from_args,
|
|
17
|
+
resolve_store,
|
|
18
|
+
resolve_user_id_from_args,
|
|
19
|
+
resolve_work_session_id_from_args,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
24
|
+
"""Register the ``revert`` subcommand parser."""
|
|
25
|
+
p = subparsers.add_parser(
|
|
26
|
+
"revert",
|
|
27
|
+
help="Revert a commit (or transition) and record a arctx transition",
|
|
28
|
+
)
|
|
29
|
+
g = p.add_mutually_exclusive_group(required=True)
|
|
30
|
+
g.add_argument("--sha", default=None, help="Commit sha to revert")
|
|
31
|
+
g.add_argument("--transition", default=None, help="Transition id (lookup latest sha)")
|
|
32
|
+
p.add_argument("-m", "--message", default=None, help="Override commit message")
|
|
33
|
+
p.add_argument("--branch", default=None, help="Override branch name")
|
|
34
|
+
p.add_argument("--run", default=None, help="Explicit run id")
|
|
35
|
+
p.add_argument("--store-dir", default=None, help="Store directory")
|
|
36
|
+
p.add_argument("--user", default=None, help="User id for attribution")
|
|
37
|
+
p.add_argument("--work-session", default=None, help="Work session id")
|
|
38
|
+
return p
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_revert_command(
|
|
42
|
+
*,
|
|
43
|
+
target_sha: str | None,
|
|
44
|
+
target_transition: str | None,
|
|
45
|
+
message: str | None,
|
|
46
|
+
branch: str | None,
|
|
47
|
+
run_id: str | None,
|
|
48
|
+
store_dir: str | None,
|
|
49
|
+
user_id: str | None,
|
|
50
|
+
work_session_id: str | None,
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""Execute a revert and persist the resulting graph records.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
target_sha:
|
|
57
|
+
Commit SHA to revert. Mutually exclusive with target_transition.
|
|
58
|
+
target_transition:
|
|
59
|
+
Transition ID whose latest sha to revert.
|
|
60
|
+
message:
|
|
61
|
+
Override commit message.
|
|
62
|
+
branch:
|
|
63
|
+
Branch name override.
|
|
64
|
+
run_id:
|
|
65
|
+
Explicit run id.
|
|
66
|
+
store_dir:
|
|
67
|
+
Store directory.
|
|
68
|
+
user_id:
|
|
69
|
+
User id for work event attribution.
|
|
70
|
+
work_session_id:
|
|
71
|
+
Work session id.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
dict with transition_id, output_node_id, branch, head_commit,
|
|
76
|
+
reverted_transition, reverted_commit.
|
|
77
|
+
"""
|
|
78
|
+
store = resolve_store(store_dir)
|
|
79
|
+
handle = store.load_run(run_id)
|
|
80
|
+
|
|
81
|
+
before = graph_counts(handle)
|
|
82
|
+
|
|
83
|
+
transition = handle.git.revert(
|
|
84
|
+
target_sha=target_sha,
|
|
85
|
+
target_transition=target_transition,
|
|
86
|
+
message=message,
|
|
87
|
+
branch=branch,
|
|
88
|
+
user_id=user_id,
|
|
89
|
+
work_session_id=work_session_id,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
maybe_append_or_save(
|
|
93
|
+
store=store,
|
|
94
|
+
handle=handle,
|
|
95
|
+
user_id=user_id,
|
|
96
|
+
work_session_id=work_session_id,
|
|
97
|
+
before=before,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Extract payload info for the result.
|
|
101
|
+
git_payloads = handle.run_graph.payloads_for_transition(
|
|
102
|
+
transition.transition_id, payload_type="git_change"
|
|
103
|
+
)
|
|
104
|
+
head_commit = git_payloads[-1].head_commit if git_payloads else ""
|
|
105
|
+
branch_payloads = handle.run_graph.payloads_for_transition(
|
|
106
|
+
transition.transition_id, payload_type="branch"
|
|
107
|
+
)
|
|
108
|
+
resolved_branch = branch_payloads[-1].branch if branch_payloads else ""
|
|
109
|
+
revert_payloads = handle.run_graph.payloads_for_transition(
|
|
110
|
+
transition.transition_id, payload_type="revert"
|
|
111
|
+
)
|
|
112
|
+
reverted_transition = revert_payloads[-1].reverted_transition if revert_payloads else ""
|
|
113
|
+
reverted_commit = revert_payloads[-1].reverted_commit if revert_payloads else ""
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"transition_id": transition.transition_id,
|
|
117
|
+
"output_node_id": transition.output_node_id,
|
|
118
|
+
"branch": resolved_branch,
|
|
119
|
+
"head_commit": head_commit,
|
|
120
|
+
"reverted_transition": reverted_transition,
|
|
121
|
+
"reverted_commit": reverted_commit,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cli_revert(args) -> int:
|
|
126
|
+
"""Entry point for ``arctx revert`` subcommand."""
|
|
127
|
+
from arctx.ext.git.verbs._forward_transition import ParallelSessionConflict # noqa: PLC0415
|
|
128
|
+
|
|
129
|
+
run_id = resolve_run_id_from_args(args)
|
|
130
|
+
user_id = resolve_user_id_from_args(args)
|
|
131
|
+
work_session_id = resolve_work_session_id_from_args(args)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
result = run_revert_command(
|
|
135
|
+
target_sha=args.sha,
|
|
136
|
+
target_transition=args.transition,
|
|
137
|
+
message=args.message,
|
|
138
|
+
branch=args.branch,
|
|
139
|
+
run_id=run_id,
|
|
140
|
+
store_dir=args.store_dir,
|
|
141
|
+
user_id=user_id,
|
|
142
|
+
work_session_id=work_session_id,
|
|
143
|
+
)
|
|
144
|
+
except ParallelSessionConflict as exc:
|
|
145
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
146
|
+
print(
|
|
147
|
+
"hint: another session has advanced this branch. "
|
|
148
|
+
"Rebase / pull before committing.",
|
|
149
|
+
file=sys.stderr,
|
|
150
|
+
)
|
|
151
|
+
return 2
|
|
152
|
+
except Exception as exc: # noqa: BLE001
|
|
153
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
print(json.dumps(result, indent=2))
|
|
157
|
+
return 0
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""arctx CLI verify command.
|
|
2
|
+
|
|
3
|
+
Validates the Descendant constraint (REDESIGN §10.9 invariant 7) over all
|
|
4
|
+
non-cut transitions in the current run.
|
|
5
|
+
|
|
6
|
+
Exit codes:
|
|
7
|
+
0 — no violations
|
|
8
|
+
1 — one or more violations (or command error)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from arctx_cli.context import resolve_run_id_from_args, resolve_store
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
22
|
+
"""Register the ``verify`` subcommand parser."""
|
|
23
|
+
p = subparsers.add_parser(
|
|
24
|
+
"verify",
|
|
25
|
+
help="Verify the descendant constraint over all transitions",
|
|
26
|
+
)
|
|
27
|
+
p.add_argument(
|
|
28
|
+
"--repo",
|
|
29
|
+
default=None,
|
|
30
|
+
help="Repo path (default: cwd)",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument("--run", default=None, help="Explicit run id")
|
|
33
|
+
p.add_argument("--store-dir", default=None, help="Store directory")
|
|
34
|
+
p.add_argument(
|
|
35
|
+
"--json",
|
|
36
|
+
action="store_true",
|
|
37
|
+
dest="json_output",
|
|
38
|
+
help="Output results as JSON",
|
|
39
|
+
)
|
|
40
|
+
return p
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_verify_command(
|
|
44
|
+
*,
|
|
45
|
+
run_id: str | None,
|
|
46
|
+
store_dir: str | None,
|
|
47
|
+
repo_path: Path | None = None,
|
|
48
|
+
skip_dead_sha_check: bool = False,
|
|
49
|
+
) -> dict:
|
|
50
|
+
"""Execute verify and return a result dict.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
run_id:
|
|
55
|
+
Explicit run id (or None to auto-resolve).
|
|
56
|
+
store_dir:
|
|
57
|
+
Store directory (or None to use ARCTX_HOME default).
|
|
58
|
+
repo_path:
|
|
59
|
+
Path to git repo root. Defaults to cwd.
|
|
60
|
+
skip_dead_sha_check:
|
|
61
|
+
If True, skip ``git cat-file -e`` pre-checks. For testing.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
dict with keys:
|
|
66
|
+
violations: list of violation dicts
|
|
67
|
+
summary: {"checked": int, "violations": int, "by_kind": {kind: int}}
|
|
68
|
+
"""
|
|
69
|
+
store = resolve_store(store_dir)
|
|
70
|
+
handle = store.load_run(run_id)
|
|
71
|
+
|
|
72
|
+
violations = handle.git.verify(
|
|
73
|
+
repo_path=repo_path,
|
|
74
|
+
skip_dead_sha_check=skip_dead_sha_check,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Count non-cut transitions that were checked.
|
|
78
|
+
from arctx.core.cuts import inactive_transition_ids # noqa: PLC0415
|
|
79
|
+
graph = handle.run_graph
|
|
80
|
+
inactive = inactive_transition_ids(graph)
|
|
81
|
+
checked = sum(1 for t_id in graph.transitions if t_id not in inactive)
|
|
82
|
+
|
|
83
|
+
by_kind: dict[str, int] = {}
|
|
84
|
+
for v in violations:
|
|
85
|
+
by_kind[v.kind] = by_kind.get(v.kind, 0) + 1
|
|
86
|
+
|
|
87
|
+
violation_dicts = [
|
|
88
|
+
{
|
|
89
|
+
"transition_id": v.transition_id,
|
|
90
|
+
"kind": v.kind,
|
|
91
|
+
"message": v.message,
|
|
92
|
+
"details": v.details,
|
|
93
|
+
}
|
|
94
|
+
for v in violations
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"violations": violation_dicts,
|
|
99
|
+
"summary": {
|
|
100
|
+
"checked": checked,
|
|
101
|
+
"violations": len(violations),
|
|
102
|
+
"by_kind": by_kind,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cli_verify(args) -> int:
|
|
108
|
+
"""Entry point for ``arctx verify`` subcommand."""
|
|
109
|
+
run_id = resolve_run_id_from_args(args)
|
|
110
|
+
repo_path = Path(args.repo) if args.repo else None
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
result = run_verify_command(
|
|
114
|
+
run_id=run_id,
|
|
115
|
+
store_dir=args.store_dir,
|
|
116
|
+
repo_path=repo_path,
|
|
117
|
+
)
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
if args.json_output:
|
|
123
|
+
print(json.dumps(result, indent=2))
|
|
124
|
+
else:
|
|
125
|
+
summary = result["summary"]
|
|
126
|
+
violations = result["violations"]
|
|
127
|
+
if not violations:
|
|
128
|
+
print(
|
|
129
|
+
f"ok: {summary['checked']} transition(s) checked, "
|
|
130
|
+
"no violations found"
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
print(
|
|
134
|
+
f"FAIL: {summary['violations']} violation(s) in "
|
|
135
|
+
f"{summary['checked']} transition(s) checked"
|
|
136
|
+
)
|
|
137
|
+
for v in violations:
|
|
138
|
+
print(f" [{v['kind']}] {v['transition_id']}: {v['message']}")
|
|
139
|
+
|
|
140
|
+
return 0 if not result["violations"] else 1
|