jj-stack 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.
- jj_stack/__init__.py +11 -0
- jj_stack/__main__.py +8 -0
- jj_stack/bootstrap.py +213 -0
- jj_stack/cli.py +1297 -0
- jj_stack/cli_help.py +680 -0
- jj_stack/commands/__init__.py +1 -0
- jj_stack/commands/_cleanup_actions.py +389 -0
- jj_stack/commands/_json_status.py +84 -0
- jj_stack/commands/checkout.py +668 -0
- jj_stack/commands/cleanup/command.py +702 -0
- jj_stack/commands/cleanup/shared.py +62 -0
- jj_stack/commands/cleanup/stale.py +79 -0
- jj_stack/commands/doctor.py +358 -0
- jj_stack/commands/in_use.py +31 -0
- jj_stack/commands/list_.py +667 -0
- jj_stack/commands/merge/__init__.py +1 -0
- jj_stack/commands/merge/command.py +385 -0
- jj_stack/commands/merge/github_stack.py +333 -0
- jj_stack/commands/merge/models.py +97 -0
- jj_stack/commands/merge/plan.py +109 -0
- jj_stack/commands/merge/preconditions.py +162 -0
- jj_stack/commands/merge/render.py +52 -0
- jj_stack/commands/relink.py +210 -0
- jj_stack/commands/submit/__init__.py +1 -0
- jj_stack/commands/submit/auto_close.py +108 -0
- jj_stack/commands/submit/changes.py +90 -0
- jj_stack/commands/submit/command.py +780 -0
- jj_stack/commands/submit/descriptions.py +570 -0
- jj_stack/commands/submit/github_stack.py +139 -0
- jj_stack/commands/submit/inputs.py +168 -0
- jj_stack/commands/submit/models.py +198 -0
- jj_stack/commands/submit/overview_comments.py +178 -0
- jj_stack/commands/submit/prs.py +461 -0
- jj_stack/commands/submit/render.py +129 -0
- jj_stack/commands/sync.py +510 -0
- jj_stack/commands/sync_apply.py +474 -0
- jj_stack/commands/unstack.py +321 -0
- jj_stack/commands/view.py +1151 -0
- jj_stack/completion.py +484 -0
- jj_stack/concurrency.py +93 -0
- jj_stack/config.py +170 -0
- jj_stack/console.py +708 -0
- jj_stack/errors.py +152 -0
- jj_stack/formatting.py +95 -0
- jj_stack/github/__init__.py +1 -0
- jj_stack/github/auth.py +34 -0
- jj_stack/github/client.py +1098 -0
- jj_stack/github/error_messages.py +89 -0
- jj_stack/github/overview_comments.py +35 -0
- jj_stack/github/pr_refs.py +70 -0
- jj_stack/github/resolution.py +208 -0
- jj_stack/github/stack_availability.py +30 -0
- jj_stack/identifiers.py +7 -0
- jj_stack/jj/__init__.py +1 -0
- jj_stack/jj/cli_args.py +23 -0
- jj_stack/jj/client.py +1459 -0
- jj_stack/jj/colors.py +199 -0
- jj_stack/models/__init__.py +1 -0
- jj_stack/models/git.py +15 -0
- jj_stack/models/github.py +243 -0
- jj_stack/models/stack.py +70 -0
- jj_stack/models/tracking.py +115 -0
- jj_stack/pr_branch_namespace.py +78 -0
- jj_stack/stack/__init__.py +1 -0
- jj_stack/stack/change_status.py +216 -0
- jj_stack/stack/convergence.py +501 -0
- jj_stack/stack/convergence_models.py +69 -0
- jj_stack/stack/convergence_observation.py +162 -0
- jj_stack/stack/github_stack_safety.py +132 -0
- jj_stack/stack/global_convergence.py +270 -0
- jj_stack/stack/path.py +235 -0
- jj_stack/stack/pr_branches.py +126 -0
- jj_stack/stack/pr_facts.py +165 -0
- jj_stack/stack/repo.py +95 -0
- jj_stack/stack/selected.py +358 -0
- jj_stack/stack/selection.py +124 -0
- jj_stack/stack/status.py +711 -0
- jj_stack/stack/trunk_evidence.py +174 -0
- jj_stack/state/__init__.py +1 -0
- jj_stack/state/operation_lock.py +220 -0
- jj_stack/state/store.py +260 -0
- jj_stack/ui.py +230 -0
- jj_stack-0.1.0.dist-info/METADATA +170 -0
- jj_stack-0.1.0.dist-info/RECORD +88 -0
- jj_stack-0.1.0.dist-info/WHEEL +4 -0
- jj_stack-0.1.0.dist-info/entry_points.txt +2 -0
- jj_stack-0.1.0.dist-info/licenses/LICENSE +201 -0
- jj_stack-0.1.0.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Observe and project one ordinary selected stack path."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import jj_stack.ui as ui
|
|
9
|
+
from jj_stack.errors import CliError
|
|
10
|
+
from jj_stack.jj.client import (
|
|
11
|
+
JjClient,
|
|
12
|
+
JjCommandError,
|
|
13
|
+
UnsupportedStackError,
|
|
14
|
+
divergent_change_id_from_error,
|
|
15
|
+
)
|
|
16
|
+
from jj_stack.models.stack import LocalCommit
|
|
17
|
+
from jj_stack.models.tracking import TrackingState
|
|
18
|
+
from jj_stack.stack.path import (
|
|
19
|
+
SelectedPathObservation,
|
|
20
|
+
SelectedStackPath,
|
|
21
|
+
project_selected_path,
|
|
22
|
+
)
|
|
23
|
+
from jj_stack.stack.pr_branches import prepare_visible_pr_snapshots
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class _ObservedPathRow:
|
|
28
|
+
"""One commit with named membership in the path observation revsets."""
|
|
29
|
+
|
|
30
|
+
commit: LocalCommit
|
|
31
|
+
is_trunk: bool
|
|
32
|
+
is_selector: bool
|
|
33
|
+
is_linked_selector: bool
|
|
34
|
+
is_candidate: bool
|
|
35
|
+
is_path: bool
|
|
36
|
+
is_trunk_path: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def select_stack_path(
|
|
40
|
+
*,
|
|
41
|
+
inspection_mode: bool = False,
|
|
42
|
+
jj_client: JjClient,
|
|
43
|
+
state: TrackingState,
|
|
44
|
+
revset: str | None = None,
|
|
45
|
+
) -> SelectedStackPath:
|
|
46
|
+
"""Collect the bounded facts for one selector and project its parent path."""
|
|
47
|
+
|
|
48
|
+
prepare_visible_pr_snapshots(jj_client=jj_client, state=state)
|
|
49
|
+
|
|
50
|
+
if revset is None:
|
|
51
|
+
selector = "@ | @-"
|
|
52
|
+
selected_revset = "@"
|
|
53
|
+
select_mutable_copy = False
|
|
54
|
+
elif _is_full_change_id(revset):
|
|
55
|
+
selector = _change_id_revset(revset)
|
|
56
|
+
selected_revset = revset
|
|
57
|
+
select_mutable_copy = True
|
|
58
|
+
else:
|
|
59
|
+
selector = revset
|
|
60
|
+
selected_revset = revset
|
|
61
|
+
select_mutable_copy = False
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
rows = _observe_path_rows(
|
|
65
|
+
jj_client=jj_client,
|
|
66
|
+
selector=selector,
|
|
67
|
+
selected_revset=revset,
|
|
68
|
+
)
|
|
69
|
+
except JjCommandError as error:
|
|
70
|
+
if revset is None or divergent_change_id_from_error(error) != revset:
|
|
71
|
+
raise
|
|
72
|
+
selector = _change_id_revset(revset)
|
|
73
|
+
select_mutable_copy = True
|
|
74
|
+
rows = _observe_path_rows(
|
|
75
|
+
jj_client=jj_client,
|
|
76
|
+
selector=selector,
|
|
77
|
+
selected_revset=revset,
|
|
78
|
+
)
|
|
79
|
+
path = _project_rows(
|
|
80
|
+
rows=rows,
|
|
81
|
+
selected_revset=selected_revset,
|
|
82
|
+
select_mutable_copy=select_mutable_copy,
|
|
83
|
+
selector_commits=tuple(row.commit for row in rows if row.is_selector),
|
|
84
|
+
state=state,
|
|
85
|
+
use_default=revset is None,
|
|
86
|
+
inspection_mode=inspection_mode,
|
|
87
|
+
)
|
|
88
|
+
if revset is None and path.stack.head.current_working_copy:
|
|
89
|
+
return _replace_selected_revset(path, "@")
|
|
90
|
+
if revset is None:
|
|
91
|
+
return _replace_selected_revset(path, "@-")
|
|
92
|
+
return path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def select_stack_path_containing_change(
|
|
96
|
+
*,
|
|
97
|
+
inspection_mode: bool = False,
|
|
98
|
+
change_id: str,
|
|
99
|
+
jj_client: JjClient,
|
|
100
|
+
state: TrackingState,
|
|
101
|
+
) -> SelectedStackPath:
|
|
102
|
+
"""Project the unique ordinary path whose head descends from one tracked change."""
|
|
103
|
+
|
|
104
|
+
prepare_visible_pr_snapshots(jj_client=jj_client, state=state)
|
|
105
|
+
linked_selector = _change_id_revset(change_id)
|
|
106
|
+
trunk_path = "first_ancestors(trunk())"
|
|
107
|
+
nonempty_descendants = f"((({linked_selector}) ~ {trunk_path}):: ~ {trunk_path}) ~ empty()"
|
|
108
|
+
selected_empty_change = f"({linked_selector}) & empty()"
|
|
109
|
+
containing_heads = f"heads(({nonempty_descendants}) | ({selected_empty_change}))"
|
|
110
|
+
rows = _observe_path_rows(
|
|
111
|
+
jj_client=jj_client,
|
|
112
|
+
linked_selector=linked_selector,
|
|
113
|
+
selector=containing_heads,
|
|
114
|
+
selected_revset=change_id,
|
|
115
|
+
)
|
|
116
|
+
selected_change_path = _project_rows(
|
|
117
|
+
rows=rows,
|
|
118
|
+
selected_revset=change_id,
|
|
119
|
+
select_mutable_copy=True,
|
|
120
|
+
selector_commits=tuple(row.commit for row in rows if row.is_linked_selector),
|
|
121
|
+
state=state,
|
|
122
|
+
use_default=False,
|
|
123
|
+
inspection_mode=inspection_mode,
|
|
124
|
+
)
|
|
125
|
+
heads = tuple(row.commit for row in rows if row.is_selector)
|
|
126
|
+
containing_heads = _heads_containing_commit(
|
|
127
|
+
commit_id=selected_change_path.stack.head.commit_id,
|
|
128
|
+
heads=heads,
|
|
129
|
+
commits=tuple(row.commit for row in rows),
|
|
130
|
+
)
|
|
131
|
+
selected_revset = containing_heads[0].change_id if len(containing_heads) == 1 else change_id
|
|
132
|
+
return _project_rows(
|
|
133
|
+
candidate_commit_ids=frozenset(head.commit_id for head in containing_heads),
|
|
134
|
+
rows=rows,
|
|
135
|
+
selected_revset=selected_revset,
|
|
136
|
+
select_mutable_copy=False,
|
|
137
|
+
selector_commits=tuple(row.commit for row in rows if row.is_selector),
|
|
138
|
+
state=state,
|
|
139
|
+
use_default=False,
|
|
140
|
+
inspection_mode=inspection_mode,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def require_submittable_changes(changes: tuple[LocalCommit, ...]) -> None:
|
|
145
|
+
"""Require the ordinary mutable changes accepted for publishing or relinking."""
|
|
146
|
+
|
|
147
|
+
for change in changes:
|
|
148
|
+
if change.immutable:
|
|
149
|
+
raise UnsupportedStackError.stack_shape(
|
|
150
|
+
change.change_id,
|
|
151
|
+
"immutable changes are not submittable.",
|
|
152
|
+
reason="immutable_commit",
|
|
153
|
+
)
|
|
154
|
+
if change.divergent:
|
|
155
|
+
raise UnsupportedStackError.stack_shape(
|
|
156
|
+
change.change_id,
|
|
157
|
+
"divergent changes are not supported.",
|
|
158
|
+
reason="divergent_change",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _observe_path_rows(
|
|
163
|
+
*,
|
|
164
|
+
jj_client: JjClient,
|
|
165
|
+
linked_selector: str | None = None,
|
|
166
|
+
selector: str,
|
|
167
|
+
selected_revset: str | None,
|
|
168
|
+
) -> tuple[_ObservedPathRow, ...]:
|
|
169
|
+
trunk_path = "first_ancestors(trunk())"
|
|
170
|
+
off_trunk = f"({selector}) ~ {trunk_path}"
|
|
171
|
+
ancestors = f"first_ancestors({off_trunk})"
|
|
172
|
+
trunk_boundaries = f"parents(({ancestors}) ~ {trunk_path}) & {trunk_path}"
|
|
173
|
+
candidate_neighborhood = f"(visible() & (({selector}) | children({selector})))"
|
|
174
|
+
candidate_commits = (
|
|
175
|
+
f"((({candidate_neighborhood}) ~ {trunk_path} ~ working_copies()) "
|
|
176
|
+
f"| (@ & {candidate_neighborhood}))"
|
|
177
|
+
)
|
|
178
|
+
linked_selector_membership = linked_selector or "none()"
|
|
179
|
+
query = " | ".join(
|
|
180
|
+
(
|
|
181
|
+
"trunk()",
|
|
182
|
+
f"({selector})",
|
|
183
|
+
f"({ancestors}) ~ {trunk_path}",
|
|
184
|
+
trunk_boundaries,
|
|
185
|
+
candidate_commits,
|
|
186
|
+
*((linked_selector,) if linked_selector is not None else ()),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
raw_rows = jj_client.query_commits_with_membership(
|
|
190
|
+
query,
|
|
191
|
+
membership_revsets=(
|
|
192
|
+
"trunk()",
|
|
193
|
+
selector,
|
|
194
|
+
linked_selector_membership,
|
|
195
|
+
candidate_commits,
|
|
196
|
+
ancestors,
|
|
197
|
+
trunk_path,
|
|
198
|
+
),
|
|
199
|
+
selected_revset=selected_revset,
|
|
200
|
+
)
|
|
201
|
+
return tuple(
|
|
202
|
+
_ObservedPathRow(
|
|
203
|
+
commit=commit,
|
|
204
|
+
is_trunk=is_trunk,
|
|
205
|
+
is_selector=is_selector,
|
|
206
|
+
is_linked_selector=is_linked_selector,
|
|
207
|
+
is_candidate=is_candidate,
|
|
208
|
+
is_path=is_path,
|
|
209
|
+
is_trunk_path=is_trunk_path,
|
|
210
|
+
)
|
|
211
|
+
for commit, (
|
|
212
|
+
is_trunk,
|
|
213
|
+
is_selector,
|
|
214
|
+
is_linked_selector,
|
|
215
|
+
is_candidate,
|
|
216
|
+
is_path,
|
|
217
|
+
is_trunk_path,
|
|
218
|
+
) in raw_rows
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _project_rows(
|
|
223
|
+
*,
|
|
224
|
+
candidate_commit_ids: frozenset[str] | None = None,
|
|
225
|
+
inspection_mode: bool,
|
|
226
|
+
rows: tuple[_ObservedPathRow, ...],
|
|
227
|
+
selected_revset: str,
|
|
228
|
+
select_mutable_copy: bool,
|
|
229
|
+
selector_commits: tuple[LocalCommit, ...],
|
|
230
|
+
state: TrackingState,
|
|
231
|
+
use_default: bool,
|
|
232
|
+
) -> SelectedStackPath:
|
|
233
|
+
trunks = tuple(row.commit for row in rows if row.is_trunk)
|
|
234
|
+
if len(trunks) != 1:
|
|
235
|
+
raise CliError(t"Could not resolve {ui.revset('trunk()')} to one commit.")
|
|
236
|
+
trunk = trunks[0]
|
|
237
|
+
if not trunk.parents:
|
|
238
|
+
raise UnsupportedStackError(
|
|
239
|
+
"No trunk bookmark is configured for this repo.",
|
|
240
|
+
hint=t"Create a trunk bookmark such as {ui.bookmark('main')}, then retry.",
|
|
241
|
+
reason="trunk_resolved_to_root",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
candidates = tuple(
|
|
245
|
+
commit
|
|
246
|
+
for commit in selector_commits
|
|
247
|
+
if candidate_commit_ids is None or commit.commit_id in candidate_commit_ids
|
|
248
|
+
)
|
|
249
|
+
if not candidates:
|
|
250
|
+
raise CliError(
|
|
251
|
+
t"Revset {ui.revset(selected_revset)} did not resolve to a visible commit."
|
|
252
|
+
)
|
|
253
|
+
current_working_copy_commit_id = (
|
|
254
|
+
next(
|
|
255
|
+
(commit.commit_id for commit in candidates if commit.current_working_copy),
|
|
256
|
+
None,
|
|
257
|
+
)
|
|
258
|
+
if use_default
|
|
259
|
+
else None
|
|
260
|
+
)
|
|
261
|
+
path_commits = tuple(row.commit for row in rows if row.is_path)
|
|
262
|
+
if not inspection_mode and any(len(commit.parents) > 1 for commit in path_commits):
|
|
263
|
+
raise UnsupportedStackError(
|
|
264
|
+
"Unsupported stack shape: merge changes are not supported.",
|
|
265
|
+
reason="merge_commit",
|
|
266
|
+
)
|
|
267
|
+
if any(not commit.parents for commit in path_commits):
|
|
268
|
+
raise UnsupportedStackError(
|
|
269
|
+
t"Unsupported stack shape: selected-parent path reached the root commit before "
|
|
270
|
+
t"{ui.revset('trunk()')}.",
|
|
271
|
+
reason="reached_root_before_trunk",
|
|
272
|
+
)
|
|
273
|
+
path = project_selected_path(
|
|
274
|
+
SelectedPathObservation(
|
|
275
|
+
candidate_commit_ids=frozenset(
|
|
276
|
+
row.commit.commit_id for row in rows if row.is_candidate
|
|
277
|
+
),
|
|
278
|
+
current_working_copy_commit_id=current_working_copy_commit_id,
|
|
279
|
+
fetched_trunk_commit_ids=frozenset(
|
|
280
|
+
row.commit.commit_id for row in rows if row.is_trunk_path
|
|
281
|
+
),
|
|
282
|
+
commits=tuple(row.commit for row in rows),
|
|
283
|
+
selected_revset=selected_revset,
|
|
284
|
+
selector_commits=candidates,
|
|
285
|
+
select_mutable_copy=select_mutable_copy,
|
|
286
|
+
trunk=trunk,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
_validate_selected_path(path, inspection_mode=inspection_mode)
|
|
290
|
+
return path
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _heads_containing_commit(
|
|
294
|
+
*,
|
|
295
|
+
commit_id: str,
|
|
296
|
+
heads: tuple[LocalCommit, ...],
|
|
297
|
+
commits: tuple[LocalCommit, ...],
|
|
298
|
+
) -> tuple[LocalCommit, ...]:
|
|
299
|
+
commits_by_id = {commit.commit_id: commit for commit in commits}
|
|
300
|
+
containing: list[LocalCommit] = []
|
|
301
|
+
for head in heads:
|
|
302
|
+
current = head
|
|
303
|
+
while current.commit_id != commit_id and current.parents:
|
|
304
|
+
parent = commits_by_id.get(current.parents[0])
|
|
305
|
+
if parent is None:
|
|
306
|
+
break
|
|
307
|
+
current = parent
|
|
308
|
+
if current.commit_id == commit_id:
|
|
309
|
+
containing.append(head)
|
|
310
|
+
return tuple(containing)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _validate_selected_path(
|
|
314
|
+
path: SelectedStackPath,
|
|
315
|
+
*,
|
|
316
|
+
inspection_mode: bool,
|
|
317
|
+
) -> None:
|
|
318
|
+
if inspection_mode:
|
|
319
|
+
return
|
|
320
|
+
for change in path.stack.changes:
|
|
321
|
+
if change.is_working_copy and change.empty:
|
|
322
|
+
raise UnsupportedStackError.stack_shape(
|
|
323
|
+
change.change_id,
|
|
324
|
+
"empty working-copy changes are not submittable.",
|
|
325
|
+
reason="empty_working_copy",
|
|
326
|
+
)
|
|
327
|
+
if change.is_working_copy and not change.description.strip():
|
|
328
|
+
raise UnsupportedStackError.stack_shape(
|
|
329
|
+
change.change_id,
|
|
330
|
+
t"describe it with {ui.cmd('jj describe')} before submitting it.",
|
|
331
|
+
reason="undescribed_working_copy",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _replace_selected_revset(
|
|
336
|
+
path: SelectedStackPath,
|
|
337
|
+
selected_revset: str,
|
|
338
|
+
) -> SelectedStackPath:
|
|
339
|
+
return SelectedStackPath(
|
|
340
|
+
is_maximal=path.is_maximal,
|
|
341
|
+
stack=path.stack.model_copy(update={"selected_revset": selected_revset}),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _change_id_revset(change_id: str) -> str:
|
|
346
|
+
return f"change_id({json.dumps(change_id)})"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _is_full_change_id(value: str) -> bool:
|
|
350
|
+
return len(value) == 32 and is_change_id_prefix(value)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def is_change_id_prefix(value: str | None) -> bool:
|
|
354
|
+
"""Return whether a bare selector has jj change-ID syntax."""
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
value is not None and bool(value) and all("k" <= character <= "z" for character in value)
|
|
358
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Change and pull-request selection helpers for command modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
import jj_stack.ui as ui
|
|
8
|
+
from jj_stack.errors import AmbiguousSelectionError, CliError, UsageError
|
|
9
|
+
from jj_stack.github.pr_refs import (
|
|
10
|
+
parse_pr_number,
|
|
11
|
+
parse_repo_pr_reference,
|
|
12
|
+
)
|
|
13
|
+
from jj_stack.github.resolution import parse_github_repo, select_submit_remote
|
|
14
|
+
from jj_stack.jj.client import JjClient
|
|
15
|
+
from jj_stack.state.store import TrackingStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_selected_revset(
|
|
19
|
+
*,
|
|
20
|
+
command_label: str,
|
|
21
|
+
default_revset: str | None = None,
|
|
22
|
+
require_explicit: bool,
|
|
23
|
+
revset: str | None,
|
|
24
|
+
) -> str | None:
|
|
25
|
+
"""Resolve an optional `<revset>` for change-oriented commands."""
|
|
26
|
+
|
|
27
|
+
if revset is not None:
|
|
28
|
+
return revset
|
|
29
|
+
if require_explicit:
|
|
30
|
+
raise UsageError(t"{ui.cmd(command_label)} requires an explicit change selection.")
|
|
31
|
+
return default_revset
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_comma_separated_flag_values(
|
|
35
|
+
values: Sequence[str] | None,
|
|
36
|
+
) -> list[str] | None:
|
|
37
|
+
"""Parse repeated comma-separated flag values into a deduplicated list."""
|
|
38
|
+
|
|
39
|
+
if values is None:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
parsed_values: list[str] = []
|
|
43
|
+
seen: set[str] = set()
|
|
44
|
+
for value in values:
|
|
45
|
+
for item in value.split(","):
|
|
46
|
+
normalized = item.strip()
|
|
47
|
+
if not normalized or normalized in seen:
|
|
48
|
+
continue
|
|
49
|
+
seen.add(normalized)
|
|
50
|
+
parsed_values.append(normalized)
|
|
51
|
+
return parsed_values
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_linked_change_for_pr(
|
|
55
|
+
*,
|
|
56
|
+
jj_client: JjClient,
|
|
57
|
+
pr_reference: str,
|
|
58
|
+
revset: str | None,
|
|
59
|
+
) -> tuple[int, str]:
|
|
60
|
+
"""Resolve `--pull-request` to one linked visible local change ID."""
|
|
61
|
+
|
|
62
|
+
if revset is not None:
|
|
63
|
+
raise UsageError(
|
|
64
|
+
t"Use either {ui.cmd('<revset>')} or {ui.cmd('--pull-request')}, not both."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
pr_number = resolve_pr_number(
|
|
68
|
+
jj_client=jj_client,
|
|
69
|
+
pr_reference=pr_reference,
|
|
70
|
+
)
|
|
71
|
+
state = TrackingStore.for_repo(jj_client.repo_root).load()
|
|
72
|
+
matching_change_ids = [
|
|
73
|
+
change_id
|
|
74
|
+
for change_id, pr_identity in state.pr_identities.items()
|
|
75
|
+
if pr_identity.pr_number == pr_number
|
|
76
|
+
]
|
|
77
|
+
if not matching_change_ids:
|
|
78
|
+
raise CliError(
|
|
79
|
+
t"PR #{pr_number} is not linked to any local change.",
|
|
80
|
+
hint=(
|
|
81
|
+
t"Use an explicit change instead, or run {ui.cmd('checkout')} or "
|
|
82
|
+
t"{ui.cmd('relink')} first."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
if len(matching_change_ids) > 1:
|
|
86
|
+
raise AmbiguousSelectionError(
|
|
87
|
+
t"PR #{pr_number} is linked to multiple local changes.",
|
|
88
|
+
hint=t"Use an explicit change ID after pointing the remote at the intended repo.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return pr_number, matching_change_ids[0]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_pr_number(
|
|
95
|
+
*,
|
|
96
|
+
jj_client: JjClient,
|
|
97
|
+
pr_reference: str,
|
|
98
|
+
) -> int:
|
|
99
|
+
"""Resolve a pull-request selector as a pull request number for this repo."""
|
|
100
|
+
|
|
101
|
+
pr_number = parse_pr_number(pr_reference)
|
|
102
|
+
if pr_number is not None:
|
|
103
|
+
return pr_number
|
|
104
|
+
|
|
105
|
+
remotes = jj_client.list_git_remotes()
|
|
106
|
+
try:
|
|
107
|
+
remote = select_submit_remote(remotes)
|
|
108
|
+
except CliError as error:
|
|
109
|
+
raise CliError(
|
|
110
|
+
t"Could not determine the GitHub repo for {ui.cmd('--pull-request')}; "
|
|
111
|
+
t"use a pull request number or fix the selected remote.",
|
|
112
|
+
hint=error.hint,
|
|
113
|
+
) from error
|
|
114
|
+
github_repo = parse_github_repo(remote)
|
|
115
|
+
if github_repo is None:
|
|
116
|
+
raise CliError(
|
|
117
|
+
t"Could not determine the GitHub repo for {ui.cmd('--pull-request')}; "
|
|
118
|
+
t"use a pull request number or fix the selected remote."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return parse_repo_pr_reference(
|
|
122
|
+
reference=pr_reference,
|
|
123
|
+
github_repo=github_repo,
|
|
124
|
+
)
|