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,62 @@
|
|
|
1
|
+
"""Shared cleanup command models, persistence, and rendering helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from jj_stack.bootstrap import CommandContext
|
|
9
|
+
from jj_stack.github.resolution import (
|
|
10
|
+
GithubTarget,
|
|
11
|
+
UnresolvedGithubTarget,
|
|
12
|
+
)
|
|
13
|
+
from jj_stack.models.git import GitRemote
|
|
14
|
+
from jj_stack.models.tracking import TrackedPR, TrackingState
|
|
15
|
+
from jj_stack.ui import Message
|
|
16
|
+
|
|
17
|
+
CleanupActionStatus = Literal["applied", "blocked", "planned", "skipped"]
|
|
18
|
+
type CleanupBody = Message
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class CleanupAction:
|
|
23
|
+
"""One cleanup action that was planned, applied, blocked, or skipped."""
|
|
24
|
+
|
|
25
|
+
kind: str
|
|
26
|
+
status: CleanupActionStatus
|
|
27
|
+
body: CleanupBody
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class CleanupResult:
|
|
32
|
+
"""Rendered cleanup result for the selected repo."""
|
|
33
|
+
|
|
34
|
+
actions: tuple[CleanupAction, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class PreparedCleanup:
|
|
39
|
+
"""Locally prepared cleanup inputs before any GitHub inspection."""
|
|
40
|
+
|
|
41
|
+
context: CommandContext
|
|
42
|
+
close_open_prs: bool
|
|
43
|
+
# None until plain cleanup proves it needs remote or GitHub state.
|
|
44
|
+
github_target: GithubTarget | UnresolvedGithubTarget | None
|
|
45
|
+
dry_run: bool
|
|
46
|
+
selected_change_ids: tuple[str, ...] | None
|
|
47
|
+
state: TrackingState
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def remote(self) -> GitRemote | None:
|
|
51
|
+
"""The selected Git remote, once remote context is loaded and one resolved."""
|
|
52
|
+
|
|
53
|
+
return self.github_target.remote if self.github_target is not None else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class PreparedCleanupChange:
|
|
58
|
+
"""Locally prepared cleanup state for one complete tracked PR."""
|
|
59
|
+
|
|
60
|
+
candidate: TrackedPR
|
|
61
|
+
has_mutable_copy: bool
|
|
62
|
+
stale_reason: str | None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Staleness detection for tracked stack changes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from jj_stack.bootstrap import CommandContext
|
|
8
|
+
from jj_stack.stack.repo import observe_repo_paths
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class LocalCleanupObservation:
|
|
13
|
+
"""Why one tracked change is stale locally, if it is."""
|
|
14
|
+
|
|
15
|
+
has_mutable_copy: bool
|
|
16
|
+
stale_reason: str | None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def local_cleanup_observations(
|
|
20
|
+
*,
|
|
21
|
+
change_ids: tuple[str, ...],
|
|
22
|
+
context: CommandContext,
|
|
23
|
+
) -> dict[str, LocalCleanupObservation]:
|
|
24
|
+
jj_client = context.jj_client
|
|
25
|
+
matched_commits = jj_client.query_commits_by_change_ids(change_ids)
|
|
26
|
+
observations: dict[str, LocalCleanupObservation] = {}
|
|
27
|
+
|
|
28
|
+
for change_id in change_ids:
|
|
29
|
+
commits = matched_commits.get(change_id, ())
|
|
30
|
+
if not commits:
|
|
31
|
+
observations[change_id] = LocalCleanupObservation(
|
|
32
|
+
has_mutable_copy=False,
|
|
33
|
+
stale_reason="no visible local copy remains",
|
|
34
|
+
)
|
|
35
|
+
continue
|
|
36
|
+
if len(commits) > 1:
|
|
37
|
+
observations[change_id] = LocalCleanupObservation(
|
|
38
|
+
has_mutable_copy=any(not commit.immutable for commit in commits),
|
|
39
|
+
stale_reason="multiple visible commits still share that change ID",
|
|
40
|
+
)
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
change = commits[0]
|
|
44
|
+
if not change.is_submittable():
|
|
45
|
+
observations[change_id] = LocalCleanupObservation(
|
|
46
|
+
has_mutable_copy=not change.immutable,
|
|
47
|
+
stale_reason="local change is no longer an active stack member",
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
observations[change_id] = LocalCleanupObservation(
|
|
52
|
+
has_mutable_copy=True,
|
|
53
|
+
stale_reason=None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
candidate_changes = tuple(
|
|
57
|
+
commits[0]
|
|
58
|
+
for change_id in change_ids
|
|
59
|
+
if observations[change_id].stale_reason is None
|
|
60
|
+
for commits in (matched_commits.get(change_id, ()),)
|
|
61
|
+
if commits
|
|
62
|
+
)
|
|
63
|
+
if not candidate_changes:
|
|
64
|
+
return observations
|
|
65
|
+
repo_paths = observe_repo_paths(
|
|
66
|
+
jj_client=jj_client,
|
|
67
|
+
descendant_of=tuple(change.commit_id for change in candidate_changes),
|
|
68
|
+
state=context.state_store.load(),
|
|
69
|
+
)
|
|
70
|
+
supported_commit_ids = {
|
|
71
|
+
change.commit_id for path in repo_paths.paths for change in path.stack.changes
|
|
72
|
+
}
|
|
73
|
+
for change in candidate_changes:
|
|
74
|
+
if change.commit_id not in supported_commit_ids:
|
|
75
|
+
observations[change.change_id] = LocalCleanupObservation(
|
|
76
|
+
has_mutable_copy=True,
|
|
77
|
+
stale_reason="local change is no longer part of a stack",
|
|
78
|
+
)
|
|
79
|
+
return observations
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Check `jj-stack`'s configuration and connectivity.
|
|
2
|
+
|
|
3
|
+
Checks PR-branch fetch settings, visible PR bookmarks, leftovers from interrupted
|
|
4
|
+
checkout or sync commands, remote selection, GitHub connectivity, authentication, and trunk
|
|
5
|
+
discovery. By default, it only reports problems. Pass `--fix` to also apply the local repairs it
|
|
6
|
+
can make safely. Currently, the only automatic repair is reserving the PR-branch namespace
|
|
7
|
+
in the remote's fetch configuration. The command never changes GitHub.
|
|
8
|
+
|
|
9
|
+
Exit status is 0 unless a check fails; warnings and problems repaired by `--fix` do not count as
|
|
10
|
+
failures. When a check fails, the command exits 1 and names a recovery command when `jj-stack` can
|
|
11
|
+
determine one.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import os
|
|
18
|
+
from contextlib import nullcontext
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Literal
|
|
22
|
+
|
|
23
|
+
import jj_stack.console as console
|
|
24
|
+
import jj_stack.ui as ui
|
|
25
|
+
from jj_stack.bootstrap import CommandContext, bootstrap_context
|
|
26
|
+
from jj_stack.errors import CliError, error_message
|
|
27
|
+
from jj_stack.github.auth import github_token, github_token_from_env
|
|
28
|
+
from jj_stack.github.client import (
|
|
29
|
+
GithubClientError,
|
|
30
|
+
build_github_client,
|
|
31
|
+
)
|
|
32
|
+
from jj_stack.github.resolution import (
|
|
33
|
+
GithubRepoAddress,
|
|
34
|
+
parse_github_repo,
|
|
35
|
+
select_submit_remote,
|
|
36
|
+
)
|
|
37
|
+
from jj_stack.github.stack_availability import github_stacks_unavailable_error
|
|
38
|
+
from jj_stack.jj.cli_args import JjCliArgs
|
|
39
|
+
from jj_stack.models.git import GitRemote
|
|
40
|
+
from jj_stack.models.github import GithubRepo
|
|
41
|
+
from jj_stack.pr_branch_namespace import current_pr_branch_namespace
|
|
42
|
+
from jj_stack.state.operation_lock import acquire_operation_lock
|
|
43
|
+
from jj_stack.ui import Message
|
|
44
|
+
|
|
45
|
+
HELP = "Check repo setup and GitHub connectivity"
|
|
46
|
+
|
|
47
|
+
type CheckDetail = Message
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True, frozen=True)
|
|
51
|
+
class CheckResult:
|
|
52
|
+
label: str
|
|
53
|
+
status: Literal["ok", "warn", "fail", "fixed", "skip"]
|
|
54
|
+
detail: CheckDetail
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def doctor(
|
|
58
|
+
*,
|
|
59
|
+
cli_args: JjCliArgs,
|
|
60
|
+
debug: bool,
|
|
61
|
+
fix: bool,
|
|
62
|
+
repo: Path | None,
|
|
63
|
+
) -> int:
|
|
64
|
+
"""CLI entrypoint for `doctor`."""
|
|
65
|
+
context = bootstrap_context(
|
|
66
|
+
repo=repo,
|
|
67
|
+
cli_args=cli_args,
|
|
68
|
+
debug=debug,
|
|
69
|
+
)
|
|
70
|
+
operation = (
|
|
71
|
+
acquire_operation_lock(
|
|
72
|
+
context.state_store.require_writable(),
|
|
73
|
+
command="doctor --fix",
|
|
74
|
+
)
|
|
75
|
+
if fix
|
|
76
|
+
else nullcontext()
|
|
77
|
+
)
|
|
78
|
+
with operation, console.spinner(description="Running checks"):
|
|
79
|
+
results = asyncio.run(_run_checks(context=context, fix=fix))
|
|
80
|
+
console.output(_results_table(results))
|
|
81
|
+
return 1 if any(r.status == "fail" for r in results) else 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _run_checks(
|
|
85
|
+
*,
|
|
86
|
+
context: CommandContext,
|
|
87
|
+
fix: bool,
|
|
88
|
+
) -> list[CheckResult]:
|
|
89
|
+
results: list[CheckResult] = []
|
|
90
|
+
|
|
91
|
+
# Check 1: Git remote selection
|
|
92
|
+
remote_result, selected_remote = _check_git_remote(context=context)
|
|
93
|
+
results.append(remote_result)
|
|
94
|
+
|
|
95
|
+
if selected_remote is None:
|
|
96
|
+
results.extend(
|
|
97
|
+
_skipped(
|
|
98
|
+
"PR branch fetch",
|
|
99
|
+
"checkout/sync leftovers",
|
|
100
|
+
"GitHub remote",
|
|
101
|
+
"GitHub auth",
|
|
102
|
+
"connectivity",
|
|
103
|
+
"GitHub stacks",
|
|
104
|
+
"trunk branch",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return results
|
|
108
|
+
|
|
109
|
+
results.append(
|
|
110
|
+
_check_pr_branch_fetch_isolation(context=context, fix=fix, remote=selected_remote)
|
|
111
|
+
)
|
|
112
|
+
results.append(_check_pr_branch_temp(context=context))
|
|
113
|
+
|
|
114
|
+
# Check 2: GitHub remote parsing
|
|
115
|
+
github_result, parsed_repo = _check_github_remote(selected_remote)
|
|
116
|
+
results.append(github_result)
|
|
117
|
+
|
|
118
|
+
if parsed_repo is None:
|
|
119
|
+
results.extend(_skipped("GitHub auth", "connectivity", "GitHub stacks", "trunk branch"))
|
|
120
|
+
return results
|
|
121
|
+
|
|
122
|
+
# Check 3: GitHub auth
|
|
123
|
+
auth_result, token = _check_github_auth()
|
|
124
|
+
results.append(auth_result)
|
|
125
|
+
|
|
126
|
+
if token is None:
|
|
127
|
+
results.extend(_skipped("connectivity", "GitHub stacks", "trunk branch"))
|
|
128
|
+
return results
|
|
129
|
+
|
|
130
|
+
# Checks 4-6: Connectivity, Stacks API availability, and trunk branch
|
|
131
|
+
connectivity_result, stacks_result, github_repo = await _check_github_connectivity(
|
|
132
|
+
parsed_repo=parsed_repo,
|
|
133
|
+
)
|
|
134
|
+
results.append(connectivity_result)
|
|
135
|
+
results.append(stacks_result)
|
|
136
|
+
|
|
137
|
+
if github_repo is not None:
|
|
138
|
+
results.append(_check_trunk_branch(github_repo))
|
|
139
|
+
else:
|
|
140
|
+
results.append(CheckResult("trunk branch", "skip", "connectivity failed"))
|
|
141
|
+
|
|
142
|
+
return results
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _skipped(*labels: str) -> list[CheckResult]:
|
|
146
|
+
return [CheckResult(label, "skip", "prior check failed") for label in labels]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_git_remote(*, context: CommandContext) -> tuple[CheckResult, GitRemote | None]:
|
|
150
|
+
jj_client = context.jj_client
|
|
151
|
+
try:
|
|
152
|
+
remotes = jj_client.list_git_remotes()
|
|
153
|
+
except Exception as error:
|
|
154
|
+
return CheckResult("remote", "fail", f"could not list remotes: {error}"), None
|
|
155
|
+
|
|
156
|
+
if not remotes:
|
|
157
|
+
return (
|
|
158
|
+
CheckResult(
|
|
159
|
+
"remote",
|
|
160
|
+
"fail",
|
|
161
|
+
t"no Git remotes configured; run {ui.cmd('jj git remote add origin <url>')} "
|
|
162
|
+
t"to add one",
|
|
163
|
+
),
|
|
164
|
+
None,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
remote = select_submit_remote(remotes)
|
|
169
|
+
except CliError as error:
|
|
170
|
+
return CheckResult("remote", "fail", error_message(error)), None
|
|
171
|
+
|
|
172
|
+
return CheckResult("remote", "ok", ui.bookmark(remote.name)), remote
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _check_github_remote(remote: GitRemote) -> tuple[CheckResult, GithubRepoAddress | None]:
|
|
176
|
+
parsed = parse_github_repo(remote)
|
|
177
|
+
if parsed is None:
|
|
178
|
+
return (
|
|
179
|
+
CheckResult(
|
|
180
|
+
"GitHub remote",
|
|
181
|
+
"fail",
|
|
182
|
+
t"remote {ui.bookmark(remote.name)} does not have fetch and push URLs "
|
|
183
|
+
t"for the same GitHub repo; use GitHub HTTPS or SSH URLs",
|
|
184
|
+
),
|
|
185
|
+
None,
|
|
186
|
+
)
|
|
187
|
+
return CheckResult("GitHub remote", "ok", parsed.full_name), parsed
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _check_pr_branch_fetch_isolation(
|
|
191
|
+
*,
|
|
192
|
+
context: CommandContext,
|
|
193
|
+
fix: bool,
|
|
194
|
+
remote: GitRemote,
|
|
195
|
+
) -> CheckResult:
|
|
196
|
+
namespace = current_pr_branch_namespace()
|
|
197
|
+
visible = tuple(context.jj_client.visible_pr_bookmark_targets())
|
|
198
|
+
visible_detail: CheckDetail = (
|
|
199
|
+
t" Visible bookmarks remain: {ui.join(ui.bookmark, visible)}." if visible else ""
|
|
200
|
+
)
|
|
201
|
+
try:
|
|
202
|
+
isolation = context.jj_client.ensure_pr_branch_fetch_isolation(
|
|
203
|
+
remote=remote.name,
|
|
204
|
+
dry_run=not fix,
|
|
205
|
+
)
|
|
206
|
+
except CliError as error:
|
|
207
|
+
detail: CheckDetail = error_message(error)
|
|
208
|
+
if error.hint is not None:
|
|
209
|
+
detail = (detail, t" {error.hint}")
|
|
210
|
+
return CheckResult("PR branch fetch", "warn", detail)
|
|
211
|
+
if isolation.status == "required":
|
|
212
|
+
if isolation.problem == "missing":
|
|
213
|
+
problem_detail = (
|
|
214
|
+
t"missing {ui.code(namespace.fetch_refspec)} exclusion; ",
|
|
215
|
+
t"add it with {ui.cmd('jj-stack doctor --fix')}.",
|
|
216
|
+
)
|
|
217
|
+
elif isolation.problem == "duplicate":
|
|
218
|
+
problem_detail = (
|
|
219
|
+
t"found multiple {ui.code(namespace.fetch_refspec)} exclusions; ",
|
|
220
|
+
t"keep one with {ui.cmd('jj-stack doctor --fix')}.",
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
raise AssertionError("required fetch isolation has no problem")
|
|
224
|
+
return CheckResult(
|
|
225
|
+
"PR branch fetch",
|
|
226
|
+
"warn",
|
|
227
|
+
(
|
|
228
|
+
problem_detail,
|
|
229
|
+
visible_detail,
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
if visible:
|
|
233
|
+
return CheckResult(
|
|
234
|
+
"PR branch fetch",
|
|
235
|
+
"warn",
|
|
236
|
+
t"exclusion is configured; visible bookmarks remain: {ui.join(ui.bookmark, visible)}",
|
|
237
|
+
)
|
|
238
|
+
return CheckResult(
|
|
239
|
+
"PR branch fetch",
|
|
240
|
+
"fixed" if isolation.status == "applied" else "ok",
|
|
241
|
+
t"exactly one {ui.code(namespace.fetch_refspec)} exclusion",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _check_pr_branch_temp(*, context: CommandContext) -> CheckResult:
|
|
246
|
+
artifacts = context.jj_client.pr_branch_temp_artifacts()
|
|
247
|
+
if artifacts.ref_target is None and not artifacts.bookmark_targets:
|
|
248
|
+
return CheckResult("checkout/sync leftovers", "ok", "none")
|
|
249
|
+
return CheckResult(
|
|
250
|
+
"checkout/sync leftovers",
|
|
251
|
+
"warn",
|
|
252
|
+
t"leftovers from an interrupted command remain; rerun the interrupted "
|
|
253
|
+
t"{ui.cmd('jj-stack checkout --pull-request PR')} or "
|
|
254
|
+
t"{ui.cmd('jj-stack sync')} command to clear it",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _check_github_auth() -> tuple[CheckResult, str | None]:
|
|
259
|
+
env_token = github_token_from_env()
|
|
260
|
+
if env_token:
|
|
261
|
+
env_var = "GITHUB_TOKEN" if os.environ.get("GITHUB_TOKEN") else "GH_TOKEN"
|
|
262
|
+
return CheckResult("GitHub auth", "ok", f"token found ({env_var})"), env_token
|
|
263
|
+
|
|
264
|
+
# Env vars not set — try the gh CLI
|
|
265
|
+
token = github_token()
|
|
266
|
+
if token:
|
|
267
|
+
return CheckResult("GitHub auth", "ok", "token found (gh CLI)"), token
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
CheckResult(
|
|
271
|
+
"GitHub auth",
|
|
272
|
+
"fail",
|
|
273
|
+
t"no token found; set GITHUB_TOKEN or run {ui.cmd('gh auth login')}",
|
|
274
|
+
),
|
|
275
|
+
None,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def _check_github_connectivity(
|
|
280
|
+
*,
|
|
281
|
+
parsed_repo: GithubRepoAddress,
|
|
282
|
+
) -> tuple[CheckResult, CheckResult, GithubRepo | None]:
|
|
283
|
+
async with build_github_client(repo=parsed_repo) as client:
|
|
284
|
+
try:
|
|
285
|
+
github_repo = await client.get_repo()
|
|
286
|
+
except GithubClientError as error:
|
|
287
|
+
return (
|
|
288
|
+
CheckResult(
|
|
289
|
+
"connectivity",
|
|
290
|
+
"fail",
|
|
291
|
+
f"{parsed_repo.full_name}: {error.user_facing_reason()}",
|
|
292
|
+
),
|
|
293
|
+
CheckResult("GitHub stacks", "skip", "connectivity failed"),
|
|
294
|
+
None,
|
|
295
|
+
)
|
|
296
|
+
except Exception as error:
|
|
297
|
+
return (
|
|
298
|
+
CheckResult(
|
|
299
|
+
"connectivity",
|
|
300
|
+
"fail",
|
|
301
|
+
f"{parsed_repo.full_name}: request failed ({error})",
|
|
302
|
+
),
|
|
303
|
+
CheckResult("GitHub stacks", "skip", "connectivity failed"),
|
|
304
|
+
None,
|
|
305
|
+
)
|
|
306
|
+
try:
|
|
307
|
+
await client.list_stacks()
|
|
308
|
+
except GithubClientError as error:
|
|
309
|
+
unavailable = github_stacks_unavailable_error(
|
|
310
|
+
error=error,
|
|
311
|
+
repo=parsed_repo.full_name,
|
|
312
|
+
)
|
|
313
|
+
detail: CheckDetail = (
|
|
314
|
+
(unavailable.message, t" {unavailable.hint}")
|
|
315
|
+
if unavailable is not None
|
|
316
|
+
else f"could not inspect stacks: {error.user_facing_reason()}"
|
|
317
|
+
)
|
|
318
|
+
stacks_result = CheckResult("GitHub stacks", "fail", detail)
|
|
319
|
+
else:
|
|
320
|
+
stacks_result = CheckResult("GitHub stacks", "ok", "Stacks API available")
|
|
321
|
+
return (
|
|
322
|
+
CheckResult(
|
|
323
|
+
"connectivity",
|
|
324
|
+
"ok",
|
|
325
|
+
f"reached {parsed_repo.full_name}",
|
|
326
|
+
),
|
|
327
|
+
stacks_result,
|
|
328
|
+
github_repo,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _check_trunk_branch(github_repo: GithubRepo) -> CheckResult:
|
|
333
|
+
if github_repo.default_branch:
|
|
334
|
+
return CheckResult("trunk branch", "ok", github_repo.default_branch)
|
|
335
|
+
return CheckResult(
|
|
336
|
+
"trunk branch",
|
|
337
|
+
"warn",
|
|
338
|
+
t"GitHub repo has no default branch set; set a default branch on GitHub "
|
|
339
|
+
t"or configure {ui.revset('trunk()')} in jj",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _results_table(results: list[CheckResult]) -> ui.DataTable:
|
|
344
|
+
return ui.DataTable(
|
|
345
|
+
columns=(
|
|
346
|
+
ui.TableColumn("check"),
|
|
347
|
+
ui.TableColumn("status", no_wrap=True),
|
|
348
|
+
ui.TableColumn("detail"),
|
|
349
|
+
),
|
|
350
|
+
rows=tuple(
|
|
351
|
+
(
|
|
352
|
+
result.label,
|
|
353
|
+
ui.status(result.status),
|
|
354
|
+
result.detail,
|
|
355
|
+
)
|
|
356
|
+
for result in results
|
|
357
|
+
),
|
|
358
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Check whether this repo contains valid `jj-stack` tracking data.
|
|
2
|
+
|
|
3
|
+
Intended for scripts and automation, the command prints nothing and exits 0 when `jj-stack` is in
|
|
4
|
+
use, 1 when it is not, and 11 for repo or tracking errors. It does not inspect the working
|
|
5
|
+
copy, read GitHub, or create tracking.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from jj_stack.bootstrap import resolve_repo_root
|
|
13
|
+
from jj_stack.errors import CliError, ProbeError
|
|
14
|
+
from jj_stack.state.store import TrackingStore
|
|
15
|
+
|
|
16
|
+
HELP = "Check whether this repo uses `jj-stack`"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def in_use(*, repo: Path | None) -> int:
|
|
20
|
+
"""CLI entrypoint for `in-use`."""
|
|
21
|
+
|
|
22
|
+
start = Path.cwd() if repo is None else repo
|
|
23
|
+
try:
|
|
24
|
+
if repo is not None and not repo.exists():
|
|
25
|
+
raise CliError(f"Repo path does not exist: {repo}")
|
|
26
|
+
if repo is not None and not repo.is_dir():
|
|
27
|
+
raise CliError(f"Repo path is not a directory: {repo}")
|
|
28
|
+
repo_root = resolve_repo_root(start)
|
|
29
|
+
return 0 if TrackingStore.for_repo(repo_root).is_in_use() else 1
|
|
30
|
+
except CliError as error:
|
|
31
|
+
raise ProbeError(error.message, hint=error.hint) from error
|