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.
Files changed (88) hide show
  1. jj_stack/__init__.py +11 -0
  2. jj_stack/__main__.py +8 -0
  3. jj_stack/bootstrap.py +213 -0
  4. jj_stack/cli.py +1297 -0
  5. jj_stack/cli_help.py +680 -0
  6. jj_stack/commands/__init__.py +1 -0
  7. jj_stack/commands/_cleanup_actions.py +389 -0
  8. jj_stack/commands/_json_status.py +84 -0
  9. jj_stack/commands/checkout.py +668 -0
  10. jj_stack/commands/cleanup/command.py +702 -0
  11. jj_stack/commands/cleanup/shared.py +62 -0
  12. jj_stack/commands/cleanup/stale.py +79 -0
  13. jj_stack/commands/doctor.py +358 -0
  14. jj_stack/commands/in_use.py +31 -0
  15. jj_stack/commands/list_.py +667 -0
  16. jj_stack/commands/merge/__init__.py +1 -0
  17. jj_stack/commands/merge/command.py +385 -0
  18. jj_stack/commands/merge/github_stack.py +333 -0
  19. jj_stack/commands/merge/models.py +97 -0
  20. jj_stack/commands/merge/plan.py +109 -0
  21. jj_stack/commands/merge/preconditions.py +162 -0
  22. jj_stack/commands/merge/render.py +52 -0
  23. jj_stack/commands/relink.py +210 -0
  24. jj_stack/commands/submit/__init__.py +1 -0
  25. jj_stack/commands/submit/auto_close.py +108 -0
  26. jj_stack/commands/submit/changes.py +90 -0
  27. jj_stack/commands/submit/command.py +780 -0
  28. jj_stack/commands/submit/descriptions.py +570 -0
  29. jj_stack/commands/submit/github_stack.py +139 -0
  30. jj_stack/commands/submit/inputs.py +168 -0
  31. jj_stack/commands/submit/models.py +198 -0
  32. jj_stack/commands/submit/overview_comments.py +178 -0
  33. jj_stack/commands/submit/prs.py +461 -0
  34. jj_stack/commands/submit/render.py +129 -0
  35. jj_stack/commands/sync.py +510 -0
  36. jj_stack/commands/sync_apply.py +474 -0
  37. jj_stack/commands/unstack.py +321 -0
  38. jj_stack/commands/view.py +1151 -0
  39. jj_stack/completion.py +484 -0
  40. jj_stack/concurrency.py +93 -0
  41. jj_stack/config.py +170 -0
  42. jj_stack/console.py +708 -0
  43. jj_stack/errors.py +152 -0
  44. jj_stack/formatting.py +95 -0
  45. jj_stack/github/__init__.py +1 -0
  46. jj_stack/github/auth.py +34 -0
  47. jj_stack/github/client.py +1098 -0
  48. jj_stack/github/error_messages.py +89 -0
  49. jj_stack/github/overview_comments.py +35 -0
  50. jj_stack/github/pr_refs.py +70 -0
  51. jj_stack/github/resolution.py +208 -0
  52. jj_stack/github/stack_availability.py +30 -0
  53. jj_stack/identifiers.py +7 -0
  54. jj_stack/jj/__init__.py +1 -0
  55. jj_stack/jj/cli_args.py +23 -0
  56. jj_stack/jj/client.py +1459 -0
  57. jj_stack/jj/colors.py +199 -0
  58. jj_stack/models/__init__.py +1 -0
  59. jj_stack/models/git.py +15 -0
  60. jj_stack/models/github.py +243 -0
  61. jj_stack/models/stack.py +70 -0
  62. jj_stack/models/tracking.py +115 -0
  63. jj_stack/pr_branch_namespace.py +78 -0
  64. jj_stack/stack/__init__.py +1 -0
  65. jj_stack/stack/change_status.py +216 -0
  66. jj_stack/stack/convergence.py +501 -0
  67. jj_stack/stack/convergence_models.py +69 -0
  68. jj_stack/stack/convergence_observation.py +162 -0
  69. jj_stack/stack/github_stack_safety.py +132 -0
  70. jj_stack/stack/global_convergence.py +270 -0
  71. jj_stack/stack/path.py +235 -0
  72. jj_stack/stack/pr_branches.py +126 -0
  73. jj_stack/stack/pr_facts.py +165 -0
  74. jj_stack/stack/repo.py +95 -0
  75. jj_stack/stack/selected.py +358 -0
  76. jj_stack/stack/selection.py +124 -0
  77. jj_stack/stack/status.py +711 -0
  78. jj_stack/stack/trunk_evidence.py +174 -0
  79. jj_stack/state/__init__.py +1 -0
  80. jj_stack/state/operation_lock.py +220 -0
  81. jj_stack/state/store.py +260 -0
  82. jj_stack/ui.py +230 -0
  83. jj_stack-0.1.0.dist-info/METADATA +170 -0
  84. jj_stack-0.1.0.dist-info/RECORD +88 -0
  85. jj_stack-0.1.0.dist-info/WHEEL +4 -0
  86. jj_stack-0.1.0.dist-info/entry_points.txt +2 -0
  87. jj_stack-0.1.0.dist-info/licenses/LICENSE +201 -0
  88. 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