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,385 @@
|
|
|
1
|
+
"""Ask GitHub to merge pull requests at the bottom of a stack.
|
|
2
|
+
|
|
3
|
+
Starting at the bottom of the stack, `jj-stack` selects consecutive open, non-draft pull
|
|
4
|
+
requests. Each must still match the commit that was last submitted; GitHub decides whether
|
|
5
|
+
reviews, checks, conflicts, and repo rules allow the merge.
|
|
6
|
+
|
|
7
|
+
For a direct merge, the command waits for GitHub to finish. It then fetches trunk, removes the
|
|
8
|
+
merged changes from the local stack, rebases any remaining changes onto the updated trunk, and
|
|
9
|
+
updates their existing pull requests.
|
|
10
|
+
|
|
11
|
+
When the trunk branch uses a merge queue, the command adds the pull requests to the queue and
|
|
12
|
+
exits once GitHub accepts them. It does not wait for them to merge or update the local stack.
|
|
13
|
+
After GitHub finishes, run `jj-stack sync <head-change-id>`.
|
|
14
|
+
|
|
15
|
+
Common examples:
|
|
16
|
+
|
|
17
|
+
- `jj-stack merge --dry-run` previews the merge without changing GitHub.
|
|
18
|
+
|
|
19
|
+
- `jj-stack merge` asks GitHub to merge the ready PRs at the bottom of the stack.
|
|
20
|
+
|
|
21
|
+
- `jj-stack merge --pull-request 123 --method squash` selects PR 123 as the last PR to merge and
|
|
22
|
+
chooses the merge method explicitly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
import jj_stack.console as console
|
|
31
|
+
import jj_stack.ui as ui
|
|
32
|
+
from jj_stack.bootstrap import CommandContext, bootstrap_context
|
|
33
|
+
from jj_stack.commands.sync import run_stack_convergence
|
|
34
|
+
from jj_stack.config import MergeMethod
|
|
35
|
+
from jj_stack.errors import CliError
|
|
36
|
+
from jj_stack.github.client import GithubClientError, build_github_client
|
|
37
|
+
from jj_stack.github.resolution import resolve_trunk_branch
|
|
38
|
+
from jj_stack.jj.cli_args import JjCliArgs
|
|
39
|
+
from jj_stack.models.github import GithubRepo
|
|
40
|
+
from jj_stack.stack.github_stack_safety import GithubStackSelection
|
|
41
|
+
from jj_stack.stack.pr_facts import observe_prs
|
|
42
|
+
from jj_stack.stack.selection import (
|
|
43
|
+
resolve_linked_change_for_pr,
|
|
44
|
+
resolve_selected_revset,
|
|
45
|
+
)
|
|
46
|
+
from jj_stack.stack.status import prepare_status
|
|
47
|
+
from jj_stack.state.operation_lock import acquire_operation_lock
|
|
48
|
+
|
|
49
|
+
from .github_stack import (
|
|
50
|
+
build_async_merge_plan,
|
|
51
|
+
execute_async_merge,
|
|
52
|
+
validate_terminal_retry,
|
|
53
|
+
)
|
|
54
|
+
from .models import MergeExecutionInputs, MergeResult, PreparedMerge
|
|
55
|
+
from .plan import build_merge_plan
|
|
56
|
+
from .render import print_merge_result
|
|
57
|
+
|
|
58
|
+
HELP = "Merge pull requests at the bottom of a stack"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def merge(
|
|
62
|
+
*,
|
|
63
|
+
cli_args: JjCliArgs,
|
|
64
|
+
debug: bool,
|
|
65
|
+
dry_run: bool,
|
|
66
|
+
merge_method: str | None,
|
|
67
|
+
pr: str | None,
|
|
68
|
+
repo: Path | None,
|
|
69
|
+
revset: str | None,
|
|
70
|
+
) -> int:
|
|
71
|
+
context = bootstrap_context(
|
|
72
|
+
repo=repo,
|
|
73
|
+
cli_args=cli_args,
|
|
74
|
+
debug=debug,
|
|
75
|
+
)
|
|
76
|
+
with acquire_operation_lock(
|
|
77
|
+
context.state_store.require_writable(),
|
|
78
|
+
command="merge",
|
|
79
|
+
):
|
|
80
|
+
return _run_merge(
|
|
81
|
+
context=context,
|
|
82
|
+
dry_run=dry_run,
|
|
83
|
+
merge_method=merge_method,
|
|
84
|
+
pr=pr,
|
|
85
|
+
revset=revset,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _run_merge(
|
|
90
|
+
*,
|
|
91
|
+
context: CommandContext,
|
|
92
|
+
dry_run: bool,
|
|
93
|
+
merge_method: str | None,
|
|
94
|
+
pr: str | None,
|
|
95
|
+
revset: str | None,
|
|
96
|
+
) -> int:
|
|
97
|
+
selected_revset, target_change_id = _resolve_merge_target(
|
|
98
|
+
context=context,
|
|
99
|
+
pr=pr,
|
|
100
|
+
revset=revset,
|
|
101
|
+
)
|
|
102
|
+
with console.spinner(description="Inspecting jj stack"):
|
|
103
|
+
prepared_merge = _prepare_merge(
|
|
104
|
+
context=context,
|
|
105
|
+
dry_run=dry_run,
|
|
106
|
+
merge_method=merge_method,
|
|
107
|
+
revset=selected_revset,
|
|
108
|
+
target_change_id=target_change_id,
|
|
109
|
+
)
|
|
110
|
+
result = asyncio.run(_stream_merge_async(prepared_merge=prepared_merge))
|
|
111
|
+
print_merge_result(result)
|
|
112
|
+
if result.blocked:
|
|
113
|
+
return 1
|
|
114
|
+
if result.enqueued or not result.applied:
|
|
115
|
+
return 0
|
|
116
|
+
sync_change_id = prepared_merge.prepared_status.prepared.stack.head.change_id
|
|
117
|
+
console.output("Updating the local stack after the completed merge:")
|
|
118
|
+
try:
|
|
119
|
+
exit_code = run_stack_convergence(
|
|
120
|
+
context=context,
|
|
121
|
+
dry_run=False,
|
|
122
|
+
fetch_remote_state=True,
|
|
123
|
+
revset=sync_change_id,
|
|
124
|
+
)
|
|
125
|
+
except BaseException as error:
|
|
126
|
+
_warn_incomplete_post_merge_sync(sync_change_id)
|
|
127
|
+
if isinstance(error, GithubClientError):
|
|
128
|
+
raise CliError(
|
|
129
|
+
"Could not update the local stack after the completed merge.",
|
|
130
|
+
hint=t"Resolve the GitHub error, then run "
|
|
131
|
+
t"{ui.cmd('jj-stack sync')} {ui.change_id(sync_change_id)}",
|
|
132
|
+
) from error
|
|
133
|
+
raise
|
|
134
|
+
if exit_code:
|
|
135
|
+
_warn_incomplete_post_merge_sync(sync_change_id)
|
|
136
|
+
return exit_code
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _warn_incomplete_post_merge_sync(sync_change_id: str) -> None:
|
|
140
|
+
console.warning(
|
|
141
|
+
t"GitHub completed the merge, but the local stack update did not finish. Do not run "
|
|
142
|
+
t"{ui.cmd('jj-stack merge')} again. Continue with "
|
|
143
|
+
t"{ui.cmd('jj-stack sync')} {ui.change_id(sync_change_id)}."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _resolve_merge_target(
|
|
148
|
+
*,
|
|
149
|
+
context: CommandContext,
|
|
150
|
+
pr: str | None,
|
|
151
|
+
revset: str | None,
|
|
152
|
+
) -> tuple[str | None, str | None]:
|
|
153
|
+
if pr is not None:
|
|
154
|
+
pr_number, resolved_revset = resolve_linked_change_for_pr(
|
|
155
|
+
jj_client=context.jj_client,
|
|
156
|
+
pr_reference=pr,
|
|
157
|
+
revset=revset,
|
|
158
|
+
)
|
|
159
|
+
console.note(t"Using PR #{pr_number} -> {ui.change_id(resolved_revset)}")
|
|
160
|
+
return None, resolved_revset
|
|
161
|
+
return (
|
|
162
|
+
resolve_selected_revset(
|
|
163
|
+
command_label="merge",
|
|
164
|
+
default_revset=None,
|
|
165
|
+
require_explicit=False,
|
|
166
|
+
revset=revset,
|
|
167
|
+
),
|
|
168
|
+
None,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _prepare_merge(
|
|
173
|
+
*,
|
|
174
|
+
context: CommandContext,
|
|
175
|
+
dry_run: bool,
|
|
176
|
+
merge_method: str | None,
|
|
177
|
+
revset: str | None,
|
|
178
|
+
target_change_id: str | None,
|
|
179
|
+
) -> PreparedMerge:
|
|
180
|
+
prepared_status = prepare_status(
|
|
181
|
+
containing_change_id=target_change_id,
|
|
182
|
+
context=context,
|
|
183
|
+
fetch_remote_state=True,
|
|
184
|
+
observe_remote_targets=False,
|
|
185
|
+
revset=revset,
|
|
186
|
+
)
|
|
187
|
+
prepared = prepared_status.prepared
|
|
188
|
+
if prepared.remote is None:
|
|
189
|
+
message = prepared.remote_error or t"Could not determine which Git remote to use."
|
|
190
|
+
raise CliError(
|
|
191
|
+
message,
|
|
192
|
+
hint=t"Configure one GitHub remote, then rerun. "
|
|
193
|
+
t"{ui.cmd('jj-stack doctor')} reports what it found.",
|
|
194
|
+
)
|
|
195
|
+
if prepared_status.github_repo is None:
|
|
196
|
+
message = prepared_status.github_repo_error or t"Could not resolve GitHub target."
|
|
197
|
+
raise CliError(
|
|
198
|
+
message,
|
|
199
|
+
hint=t"Point jj-stack at a GitHub remote, then rerun. "
|
|
200
|
+
t"{ui.cmd('jj-stack doctor')} reports what it found.",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not dry_run:
|
|
204
|
+
context.state_store.require_writable()
|
|
205
|
+
return PreparedMerge(
|
|
206
|
+
context=context,
|
|
207
|
+
dry_run=dry_run,
|
|
208
|
+
merge_method=merge_method,
|
|
209
|
+
prepared_status=prepared_status,
|
|
210
|
+
target_change_id=target_change_id,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def _stream_merge_async(
|
|
215
|
+
*,
|
|
216
|
+
prepared_merge: PreparedMerge,
|
|
217
|
+
) -> MergeResult:
|
|
218
|
+
prepared_status = prepared_merge.prepared_status
|
|
219
|
+
prepared = prepared_status.prepared
|
|
220
|
+
github_repo = prepared_status.github_repo
|
|
221
|
+
remote = prepared.remote
|
|
222
|
+
if github_repo is None or remote is None:
|
|
223
|
+
raise AssertionError("Prepared merge requires resolved GitHub and remote targets.")
|
|
224
|
+
|
|
225
|
+
async with build_github_client(repo=github_repo) as github_client:
|
|
226
|
+
with console.spinner(description="Loading remote branches"):
|
|
227
|
+
try:
|
|
228
|
+
github_repo_state, branches_at_trunk = await asyncio.gather(
|
|
229
|
+
github_client.get_repo(),
|
|
230
|
+
github_client.list_branches_for_head_commit(
|
|
231
|
+
commit_sha=prepared.stack.trunk.commit_id
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
except GithubClientError as error:
|
|
235
|
+
raise CliError(
|
|
236
|
+
t"Could not inspect GitHub repo {github_repo.full_name}",
|
|
237
|
+
hint="Resolve the GitHub error above, then rerun merge.",
|
|
238
|
+
) from error
|
|
239
|
+
trunk_branch, _trunk_targets = resolve_trunk_branch(
|
|
240
|
+
branches_at_trunk=branches_at_trunk,
|
|
241
|
+
github_repo_state=github_repo_state,
|
|
242
|
+
remote=remote,
|
|
243
|
+
trunk_commit_id=prepared.stack.trunk.commit_id,
|
|
244
|
+
)
|
|
245
|
+
try:
|
|
246
|
+
uses_merge_queue = await github_client.base_branch_uses_merge_queue(
|
|
247
|
+
branch=trunk_branch
|
|
248
|
+
)
|
|
249
|
+
except GithubClientError:
|
|
250
|
+
uses_merge_queue = False
|
|
251
|
+
if uses_merge_queue:
|
|
252
|
+
if prepared_merge.merge_method is not None:
|
|
253
|
+
console.warning(
|
|
254
|
+
t"The base branch {ui.bookmark(trunk_branch)} uses a merge queue; ignoring "
|
|
255
|
+
t"{ui.cmd('--method')}."
|
|
256
|
+
)
|
|
257
|
+
merge_action = "merge_queue"
|
|
258
|
+
resolved_merge_method = None
|
|
259
|
+
else:
|
|
260
|
+
merge_action = "direct_merge"
|
|
261
|
+
resolved_merge_method = _resolve_merge_method(
|
|
262
|
+
configured=prepared_merge.context.config.merge_method,
|
|
263
|
+
merge_method=prepared_merge.merge_method,
|
|
264
|
+
repo_state=github_repo_state,
|
|
265
|
+
)
|
|
266
|
+
try:
|
|
267
|
+
observation = await observe_prs(
|
|
268
|
+
change_ids=tuple(change.change_id for change in prepared.stack.changes),
|
|
269
|
+
context=prepared_merge.context,
|
|
270
|
+
github_client=github_client,
|
|
271
|
+
github_repo_snapshot=github_repo_state,
|
|
272
|
+
remote_name=remote.name,
|
|
273
|
+
)
|
|
274
|
+
except GithubClientError as error:
|
|
275
|
+
raise CliError(
|
|
276
|
+
"Could not inspect GitHub state for merge.",
|
|
277
|
+
hint="Resolve the GitHub error above, then rerun merge.",
|
|
278
|
+
) from error
|
|
279
|
+
|
|
280
|
+
plan = build_merge_plan(
|
|
281
|
+
observation=observation,
|
|
282
|
+
remote_name=remote.name,
|
|
283
|
+
repo=github_repo,
|
|
284
|
+
changes=prepared.stack.changes,
|
|
285
|
+
state=prepared.state,
|
|
286
|
+
target_change_id=prepared_merge.target_change_id,
|
|
287
|
+
trunk_branch=trunk_branch,
|
|
288
|
+
)
|
|
289
|
+
selection = GithubStackSelection(
|
|
290
|
+
github_client,
|
|
291
|
+
tuple(change.identity.pr_number for change in plan.linked_changes),
|
|
292
|
+
)
|
|
293
|
+
stacks = await selection.observe()
|
|
294
|
+
async_merge = build_async_merge_plan(
|
|
295
|
+
plan,
|
|
296
|
+
stacks,
|
|
297
|
+
prepared_merge.target_change_id,
|
|
298
|
+
)
|
|
299
|
+
execution = MergeExecutionInputs(
|
|
300
|
+
remote_name=remote.name,
|
|
301
|
+
selected_revset=prepared.stack.selected_revset,
|
|
302
|
+
trunk_branch=trunk_branch,
|
|
303
|
+
trunk_subject=prepared.stack.trunk.subject,
|
|
304
|
+
)
|
|
305
|
+
if async_merge.planned and async_merge.terminal_retry:
|
|
306
|
+
validate_terminal_retry(
|
|
307
|
+
execution=execution,
|
|
308
|
+
github=github_client,
|
|
309
|
+
merge=async_merge,
|
|
310
|
+
observation=observation,
|
|
311
|
+
)
|
|
312
|
+
if prepared_merge.dry_run:
|
|
313
|
+
if async_merge.planned:
|
|
314
|
+
action = async_merge.action(
|
|
315
|
+
merge_action=merge_action,
|
|
316
|
+
method=resolved_merge_method,
|
|
317
|
+
trunk_branch=trunk_branch,
|
|
318
|
+
)
|
|
319
|
+
actions = (
|
|
320
|
+
(action, async_merge.boundary_action)
|
|
321
|
+
if async_merge.boundary_action is not None
|
|
322
|
+
else (action,)
|
|
323
|
+
)
|
|
324
|
+
return execution.result(actions=actions)
|
|
325
|
+
return execution.result(
|
|
326
|
+
actions=(
|
|
327
|
+
() if async_merge.boundary_action is None else (async_merge.boundary_action,)
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
return await execute_async_merge(
|
|
331
|
+
execution=execution,
|
|
332
|
+
github=github_client,
|
|
333
|
+
merge_action=merge_action,
|
|
334
|
+
merge_method=resolved_merge_method,
|
|
335
|
+
merge=async_merge,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _resolve_merge_method(
|
|
340
|
+
*,
|
|
341
|
+
configured: MergeMethod | None,
|
|
342
|
+
merge_method: str | None,
|
|
343
|
+
repo_state: GithubRepo,
|
|
344
|
+
) -> str:
|
|
345
|
+
"""Choose the merge method, preferring this run's flag over the repo's configuration.
|
|
346
|
+
|
|
347
|
+
GitHub reports which methods a repo allows but never which one to prefer, so a repo that
|
|
348
|
+
allows several needs the choice made here.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
settings = {
|
|
352
|
+
"merge": repo_state.allow_merge_commit,
|
|
353
|
+
"rebase": repo_state.allow_rebase_merge,
|
|
354
|
+
"squash": repo_state.allow_squash_merge,
|
|
355
|
+
}
|
|
356
|
+
if any(allowed is None for allowed in settings.values()):
|
|
357
|
+
if merge_method is not None:
|
|
358
|
+
return merge_method
|
|
359
|
+
raise CliError(
|
|
360
|
+
"GitHub did not report which merge methods this repo allows.",
|
|
361
|
+
hint=t"Pass {ui.cmd('--method')} explicitly.",
|
|
362
|
+
)
|
|
363
|
+
allowed_methods = sorted(method for method, allowed in settings.items() if allowed)
|
|
364
|
+
if not allowed_methods:
|
|
365
|
+
raise CliError(
|
|
366
|
+
"This repo does not allow any pull request merge method.",
|
|
367
|
+
hint="Fix the repo merge settings on GitHub before merging.",
|
|
368
|
+
)
|
|
369
|
+
chosen = merge_method or configured
|
|
370
|
+
if chosen is not None:
|
|
371
|
+
if chosen not in allowed_methods:
|
|
372
|
+
source = ui.cmd("--method") if merge_method else ui.code("jj-stack.merge_method")
|
|
373
|
+
raise CliError(
|
|
374
|
+
t"This repo does not allow {ui.cmd(chosen)} merges; it allows "
|
|
375
|
+
t"{ui.join(ui.cmd, allowed_methods)}.",
|
|
376
|
+
hint=t"Change {source} to one it allows, or enable {ui.cmd(chosen)} on GitHub.",
|
|
377
|
+
)
|
|
378
|
+
return chosen
|
|
379
|
+
if len(allowed_methods) == 1:
|
|
380
|
+
return allowed_methods[0]
|
|
381
|
+
raise CliError(
|
|
382
|
+
t"This repo allows more than one merge method ({ui.join(ui.cmd, allowed_methods)}).",
|
|
383
|
+
hint=t"Pass {ui.cmd('--method')}, or set it once with "
|
|
384
|
+
t"{ui.cmd('jj config set --repo jj-stack.merge_method squash')}.",
|
|
385
|
+
)
|