gabriel / muse public
sparse_checkout.py python
745 lines 24.7 KB
Raw
sha256:84df9126d09aeec0b8f1b908f0b06c10913feec28f3514b382efb1ba6d619385 refactor: rename StructuredMergePlugin to AddressedMergePlu… Sonnet 4.6 minor ⚠ breaking 24 days ago
1 """``muse sparse-checkout`` — partial working-tree materialization.
2
3 Sparse-checkout lets you work with only a subset of a large repository's files
4 in your working tree. The full snapshot manifest is always stored and tracked
5 by Muse; only the files that match your sparse rules are written to disk.
6
7 Subcommands
8 -----------
9 ``init [--no-cone] [--json]``
10 Activate sparse-checkout. ``--cone`` (default) uses directory-prefix rules;
11 ``--no-cone`` uses glob patterns. If a config already exists, running init
12 again with a *different* mode switches the mode while preserving patterns.
13 Running init with the *same* mode is a no-op.
14
15 ``set <pattern...> [--json]``
16 Replace the current pattern list. Requires ``init`` first.
17
18 ``add <pattern...> [--json]``
19 Append patterns to the current list (deduplicates). Requires ``init`` first.
20
21 ``list [--json]``
22 Show the active patterns and mode.
23
24 ``stats [--json]``
25 Show how many files in the HEAD snapshot match or are excluded by the current
26 sparse config. Reports total_files, matching_files, excluded_files, and
27 efficiency (ratio of matching to total).
28
29 ``disable [--json]``
30 Remove the sparse-checkout configuration. The next ``checkout`` or ``merge``
31 will restore the full working tree.
32
33 Modes
34 -----
35 ``cone`` (default)
36 Patterns are directory prefixes, e.g. ``src/``. Root-level files always
37 match. Subdirectory files match when their path starts with a prefix.
38
39 ``pattern``
40 Patterns are glob expressions, e.g. ``**/*.py`` or ``src/**``.
41
42 Pattern safety rules
43 --------------------
44 Patterns are validated before storage. The following are rejected:
45
46 - ANSI escape sequences (terminal-injection guard)
47 - Null bytes (filesystem attack vector)
48 - Whitespace-only strings (meaningless; likely a user error)
49 - Path traversal via ``..`` segments (e.g. ``../../etc/passwd``)
50
51 JSON output schemas
52 -------------------
53
54 ``init --json``::
55
56 {"mode": str, "switched": bool, "previous_mode": str|null,
57 "duration_ms": float, "exit_code": int}
58
59 ``set --json``::
60
61 {"patterns": [str, ...], "total": int, "duration_ms": float, "exit_code": int}
62
63 ``add --json``::
64
65 {"added": int, "skipped": int, "patterns": [str, ...],
66 "total": int, "duration_ms": float, "exit_code": int}
67
68 ``list --json``::
69
70 {"enabled": bool, "mode": str|null, "patterns": [str, ...],
71 "duration_ms": float, "exit_code": int}
72
73 ``stats --json``::
74
75 {"enabled": bool, "mode": str|null, "patterns": [str, ...],
76 "total_files": int, "matching_files": int, "excluded_files": int,
77 "efficiency": float, "duration_ms": float, "exit_code": int}
78
79 ``disable --json``::
80
81 {"was_enabled": bool, "duration_ms": float, "exit_code": int}
82
83 Exit codes::
84
85 0 — success
86 1 — operation failed (no config, invalid patterns, config corruption)
87 2 — usage error
88 """
89
90 import argparse
91 import json as _json
92 import re
93 import sys
94
95 from muse.core.envelope import EnvelopeJson, make_envelope
96 from muse.core.paths import sparse_checkout_path as _sparse_checkout_path
97 from muse.core.errors import ExitCode
98 from muse.core.repo import require_repo
99 from muse.core.sparse import (
100 SparseConfig,
101 filter_manifest_sparse,
102 read_sparse_config,
103 remove_sparse_config,
104 write_sparse_config,
105 )
106 from muse.core.validation import sanitize_display
107 from muse.core.timing import start_timer
108
109 # ---------------------------------------------------------------------------
110 # Wire types
111 # ---------------------------------------------------------------------------
112
113 class _SparseInitJson(EnvelopeJson):
114 mode: str
115 switched: bool
116 previous_mode: str | None
117
118 class _SparseSetJson(EnvelopeJson):
119 patterns: list[str]
120 total: int
121
122 class _SparseAddJson(EnvelopeJson):
123 added: int
124 skipped: int
125 patterns: list[str]
126 total: int
127
128 class _SparseListJson(EnvelopeJson):
129 enabled: bool
130 mode: str | None
131 patterns: list[str]
132
133 class _SparseStatsJson(EnvelopeJson):
134 enabled: bool
135 mode: str | None
136 patterns: list[str]
137 total_files: int
138 matching_files: int
139 excluded_files: int
140 efficiency: float
141
142 class _SparseDisableJson(EnvelopeJson):
143 was_enabled: bool
144
145 # ---------------------------------------------------------------------------
146 # Pattern validation
147 # ---------------------------------------------------------------------------
148
149 _ANSI_RE = re.compile(r"\x1b|\x9b|\x1c|\x1d|\x1e|\x1f")
150
151 # Matches ".." as a path component: preceded/followed by separator or string boundary.
152 _TRAVERSAL_RE = re.compile(r"(^|[/\\])\.\.([/\\]|$)")
153
154 def _check_pattern(pat: str) -> tuple[bool, str]:
155 """Validate *pat* and return ``(ok, reason)``.
156
157 Rejects patterns that are:
158
159 - ANSI escape sequences — prevent terminal-injection when patterns are
160 printed to stdout/stderr.
161 - Null bytes — filesystem attack vector; ``open()`` rejects them on most
162 platforms, causing misleading errors deep in the call stack.
163 - Whitespace-only — meaningless as a path prefix or glob; almost certainly
164 a user error.
165 - Path traversal via ``..`` — a pattern like ``../../etc/passwd`` would
166 match files outside the repository root after prefix expansion.
167 """
168 if _ANSI_RE.search(pat):
169 return False, "ANSI escape sequence detected"
170 if "\x00" in pat:
171 return False, "null byte detected"
172 if not pat.strip():
173 return False, "empty or whitespace-only pattern"
174 if _TRAVERSAL_RE.search(pat):
175 return False, "path traversal via '..' detected"
176 if pat.strip("/\\") == "..":
177 return False, "path traversal via '..' detected"
178 return True, ""
179
180 def _validate_patterns(patterns: list[str]) -> None:
181 """Check every pattern in *patterns*, exiting on the first invalid one."""
182 for pat in patterns:
183 ok, reason = _check_pattern(pat)
184 if not ok:
185 print(
186 f"❌ Invalid pattern {sanitize_display(repr(pat))}: {reason}",
187 file=sys.stderr,
188 )
189 raise SystemExit(ExitCode.USER_ERROR)
190
191 # ---------------------------------------------------------------------------
192 # Config helpers
193 # ---------------------------------------------------------------------------
194
195 def _read_config_safe(root: pathlib.Path) -> SparseConfig | None:
196 """Read the sparse config, exiting with a clear error on JSON corruption.
197
198 ``load_json_file`` (used by ``read_sparse_config``) returns ``None`` for
199 both "file not found" and "file is corrupt JSON". We distinguish the two
200 cases by checking whether the file exists before falling back to ``None``.
201 A present-but-unreadable config is always a user-visible error.
202 """
203 import json as _stdlib_json
204 cfg_path = _sparse_checkout_path(root)
205 if not cfg_path.exists():
206 return None
207 try:
208 raw = cfg_path.read_text(encoding="utf-8")
209 data = _stdlib_json.loads(raw)
210 if not isinstance(data, dict):
211 raise ValueError(f"expected a JSON object, got {type(data).__name__}")
212 return data
213 except Exception as exc:
214 print(
215 f"❌ Sparse-checkout config is corrupted and cannot be read: {exc}",
216 file=sys.stderr,
217 )
218 raise SystemExit(ExitCode.USER_ERROR)
219
220 def _validate_config_structure(cfg: SparseConfig) -> None:
221 """Exit with USER_ERROR if *cfg* is missing required fields or has invalid values.
222
223 Called after a successful JSON parse to catch configs that are syntactically
224 valid JSON but semantically invalid for sparse-checkout (e.g. missing 'mode'
225 or 'patterns' keys, unknown mode value).
226 """
227 if "mode" not in cfg:
228 print(
229 "❌ Sparse-checkout config is missing required 'mode' field.",
230 file=sys.stderr,
231 )
232 raise SystemExit(ExitCode.USER_ERROR)
233 if cfg["mode"] not in ("cone", "pattern"):
234 print(
235 f"❌ Sparse-checkout config has invalid mode "
236 f"'{sanitize_display(str(cfg['mode']))}'. Expected 'cone' or 'pattern'.",
237 file=sys.stderr,
238 )
239 raise SystemExit(ExitCode.USER_ERROR)
240 if "patterns" not in cfg:
241 print(
242 "❌ Sparse-checkout config is missing required 'patterns' field.",
243 file=sys.stderr,
244 )
245 raise SystemExit(ExitCode.USER_ERROR)
246
247 def _require_config(root: pathlib.Path) -> SparseConfig:
248 """Return the sparse config or exit with a helpful message if not initialised."""
249 cfg = _read_config_safe(root)
250 if cfg is None:
251 print(
252 "❌ Sparse-checkout is not initialised. Run `muse sparse-checkout init` first.",
253 file=sys.stderr,
254 )
255 raise SystemExit(ExitCode.USER_ERROR)
256 _validate_config_structure(cfg)
257 return cfg
258
259 # ---------------------------------------------------------------------------
260 # Subcommand handlers
261 # ---------------------------------------------------------------------------
262
263 def _cmd_init(args: argparse.Namespace, root: pathlib.Path) -> None:
264 """Activate sparse-checkout or switch its mode.
265
266 Initialises a new sparse-checkout configuration with the requested mode
267 (``cone`` by default, ``pattern`` when ``--no-cone`` is set). When a
268 config already exists, running init again with the *same* mode is a no-op
269 (``switched=False``); a *different* mode switches the mode, preserving
270 existing patterns (``switched=True``).
271
272 Agent quickstart::
273
274 muse sparse-checkout init --json
275 muse sparse-checkout init --no-cone --json
276
277 JSON fields::
278
279 mode Active mode after this command: ``"cone"`` or ``"pattern"``.
280 switched ``true`` when the mode was changed from a previous setting.
281 previous_mode Prior mode when switched; ``null`` on first init or no-op.
282 muse_version Muse release that produced this output.
283 schema Envelope schema version (int).
284 exit_code ``0`` on success, ``1`` on config validation failure.
285 duration_ms Wall-clock milliseconds for the command.
286 timestamp ISO-8601 UTC timestamp of command completion.
287 warnings List of non-fatal advisory messages.
288
289 Exit codes::
290
291 0 Success.
292 1 User error (invalid config).
293 2 Usage error.
294 """
295 elapsed = start_timer()
296
297 json_out: bool = getattr(args, "json_out", False)
298 requested_mode = "pattern" if args.no_cone else "cone"
299
300 cfg = _read_config_safe(root)
301
302 if cfg is None:
303 write_sparse_config(root, {"mode": requested_mode, "patterns": []})
304 if json_out:
305 print(_json.dumps(_SparseInitJson(
306 **make_envelope(elapsed),
307 mode=requested_mode,
308 switched=False,
309 previous_mode=None,
310 )))
311 else:
312 print(f"Sparse-checkout enabled (mode: {requested_mode}).")
313 return
314
315 _validate_config_structure(cfg)
316 previous_mode = cfg["mode"]
317 switched = previous_mode != requested_mode
318
319 if switched:
320 cfg["mode"] = requested_mode
321 write_sparse_config(root, cfg)
322
323 if json_out:
324 print(_json.dumps(_SparseInitJson(
325 **make_envelope(elapsed),
326 mode=requested_mode,
327 switched=switched,
328 previous_mode=previous_mode if switched else None,
329 )))
330 else:
331 if switched:
332 print(f"Sparse-checkout mode switched: {previous_mode} → {requested_mode}.")
333 else:
334 print(f"Sparse-checkout already enabled (mode: {previous_mode}).")
335
336 def _cmd_set(args: argparse.Namespace, root: pathlib.Path) -> None:
337 """Replace the full pattern list.
338
339 Validates every pattern for safety before writing. Overwrites the
340 previous pattern list entirely — use ``add`` to append instead. Exits
341 with ``USER_ERROR`` if any pattern is invalid or if sparse-checkout has
342 not been initialised.
343
344 Agent quickstart::
345
346 muse sparse-checkout set 'src/' 'tests/' --json
347 muse sparse-checkout set '**/*.py' --json
348
349 JSON fields::
350
351 patterns Updated pattern list after the operation.
352 total Number of patterns now active.
353 muse_version Muse release that produced this output.
354 schema Envelope schema version (int).
355 exit_code ``0`` on success, ``1`` on error.
356 duration_ms Wall-clock milliseconds for the command.
357 timestamp ISO-8601 UTC timestamp of command completion.
358 warnings List of non-fatal advisory messages.
359
360 Exit codes::
361
362 0 Success.
363 1 User error (not initialised, invalid pattern).
364 2 Usage error.
365 """
366 elapsed = start_timer()
367
368 json_out: bool = getattr(args, "json_out", False)
369 cfg = _require_config(root)
370 _validate_patterns(args.patterns)
371
372 cfg["patterns"] = list(args.patterns)
373 write_sparse_config(root, cfg)
374
375 if json_out:
376 print(_json.dumps(_SparseSetJson(
377 **make_envelope(elapsed),
378 patterns=cfg["patterns"],
379 total=len(cfg["patterns"]),
380 )))
381 else:
382 print(f"Patterns set ({len(cfg['patterns'])} total).")
383
384 def _cmd_add(args: argparse.Namespace, root: pathlib.Path) -> None:
385 """Append new patterns, skipping duplicates.
386
387 Validates every candidate pattern before adding. Patterns already present
388 are silently counted as ``skipped``; only genuinely new patterns are stored.
389 Exits with ``USER_ERROR`` if any pattern is invalid or if sparse-checkout
390 has not been initialised.
391
392 Agent quickstart::
393
394 muse sparse-checkout add 'docs/' --json
395 muse sparse-checkout add '**/*.md' '**/*.rst' --json
396
397 JSON fields::
398
399 added Number of new patterns appended.
400 skipped Number of patterns already present (not re-added).
401 patterns Full pattern list after the operation.
402 total Total number of patterns now active.
403 muse_version Muse release that produced this output.
404 schema Envelope schema version (int).
405 exit_code ``0`` on success, ``1`` on error.
406 duration_ms Wall-clock milliseconds for the command.
407 timestamp ISO-8601 UTC timestamp of command completion.
408 warnings List of non-fatal advisory messages.
409
410 Exit codes::
411
412 0 Success.
413 1 User error (not initialised, invalid pattern).
414 2 Usage error.
415 """
416 elapsed = start_timer()
417
418 json_out: bool = getattr(args, "json_out", False)
419 cfg = _require_config(root)
420 _validate_patterns(args.patterns)
421
422 existing = set(cfg["patterns"])
423 new_pats = [p for p in args.patterns if p not in existing]
424 cfg["patterns"].extend(new_pats)
425 write_sparse_config(root, cfg)
426
427 added = len(new_pats)
428 skipped = len(args.patterns) - added
429
430 if json_out:
431 print(_json.dumps(_SparseAddJson(
432 **make_envelope(elapsed),
433 added=added,
434 skipped=skipped,
435 patterns=cfg["patterns"],
436 total=len(cfg["patterns"]),
437 )))
438 else:
439 msg = f"Added {added} pattern(s)"
440 if skipped:
441 msg += f" ({skipped} already present)"
442 print(f"{msg}.")
443
444 def _cmd_list(args: argparse.Namespace, root: pathlib.Path) -> None:
445 """Display the active patterns and mode.
446
447 Returns exit code 0 even when sparse-checkout is disabled — the absence
448 of a config is not an error, just a state. Validates the config structure
449 when a config file is present (catches post-hoc corruption).
450
451 Agent quickstart::
452
453 muse sparse-checkout list --json
454
455 JSON fields::
456
457 enabled ``true`` when sparse-checkout is configured.
458 mode ``"cone"`` or ``"pattern"`` when enabled; ``null`` when disabled.
459 patterns Active pattern list; empty list when disabled.
460 muse_version Muse release that produced this output.
461 schema Envelope schema version (int).
462 exit_code ``0`` on success, ``1`` on config corruption.
463 duration_ms Wall-clock milliseconds for the command.
464 timestamp ISO-8601 UTC timestamp of command completion.
465 warnings List of non-fatal advisory messages.
466
467 Exit codes::
468
469 0 Success (including when sparse-checkout is disabled).
470 1 User error (corrupt config).
471 """
472 elapsed = start_timer()
473
474 json_out: bool = getattr(args, "json_out", False)
475 cfg = _read_config_safe(root)
476
477 if cfg is not None:
478 _validate_config_structure(cfg)
479
480 if cfg is None:
481 if json_out:
482 print(_json.dumps(_SparseListJson(
483 **make_envelope(elapsed),
484 enabled=False,
485 mode=None,
486 patterns=[],
487 )))
488 else:
489 print("Sparse-checkout is disabled (full working tree).")
490 return
491
492 if json_out:
493 print(_json.dumps(_SparseListJson(
494 **make_envelope(elapsed),
495 enabled=True,
496 mode=cfg["mode"],
497 patterns=cfg["patterns"],
498 )))
499 else:
500 mode = cfg["mode"]
501 patterns = cfg["patterns"]
502 print(f"Mode: {mode}")
503 print(f"Patterns: {len(patterns)}")
504 if patterns:
505 print()
506 for pat in patterns:
507 print(f" {pat}")
508 else:
509 print(" (none — matches nothing)")
510
511 def _cmd_stats(args: argparse.Namespace, root: pathlib.Path) -> None:
512 """Report how many HEAD-snapshot files match the current sparse config.
513
514 Reads the HEAD commit's snapshot manifest and applies the sparse filter,
515 counting matching vs. excluded files. When sparse-checkout is disabled,
516 all files are considered matching (efficiency = 1.0). When no commits
517 exist yet, all counts are zero.
518
519 Agent quickstart::
520
521 muse sparse-checkout stats --json
522
523 JSON fields::
524
525 enabled ``true`` when a sparse-checkout config is present.
526 mode ``"cone"`` or ``"pattern"``; ``null`` when disabled.
527 patterns Active pattern list; empty list when disabled.
528 total_files Total files in the HEAD snapshot (0 if no commits).
529 matching_files Files that pass the sparse filter (or total when disabled).
530 excluded_files ``total_files - matching_files``.
531 efficiency ``matching_files / total_files``; 1.0 when disabled or no files.
532 muse_version Muse release that produced this output.
533 schema Envelope schema version (int).
534 exit_code Always ``0`` on success.
535 duration_ms Wall-clock milliseconds for the command.
536 timestamp ISO-8601 UTC timestamp of command completion.
537 warnings List of non-fatal advisory messages.
538
539 Exit codes::
540
541 0 Success.
542 1 User error (corrupt config).
543 """
544 elapsed = start_timer()
545
546 cfg = _read_config_safe(root)
547 if cfg is not None:
548 _validate_config_structure(cfg)
549
550 # Resolve the HEAD snapshot manifest.
551 total = 0
552 manifest = {}
553 try:
554 from muse.core.refs import (
555 get_head_commit_id,
556 read_current_branch,
557 )
558 from muse.core.commits import read_commit
559 from muse.core.snapshots import read_snapshot
560 branch = read_current_branch(root)
561 commit_id = get_head_commit_id(root, branch)
562 if commit_id is not None:
563 commit = read_commit(root, commit_id)
564 if commit is not None:
565 snap = read_snapshot(root, commit.snapshot_id)
566 if snap is not None:
567 manifest = snap.manifest
568 total = len(manifest)
569 except Exception:
570 pass # No commits yet — counts stay zero.
571
572 if cfg is None:
573 matching = total
574 elif total == 0:
575 matching = 0
576 else:
577 matching = len(filter_manifest_sparse(manifest, cfg["patterns"], mode=cfg["mode"]))
578
579 excluded = total - matching
580 if total > 0:
581 efficiency = round(matching / total, 6)
582 else:
583 efficiency = 1.0 if cfg is None else 0.0
584
585 print(_json.dumps(_SparseStatsJson(
586 **make_envelope(elapsed),
587 enabled=cfg is not None,
588 mode=cfg["mode"] if cfg else None,
589 patterns=cfg["patterns"] if cfg else [],
590 total_files=total,
591 matching_files=matching,
592 excluded_files=excluded,
593 efficiency=efficiency,
594 )))
595
596 def _cmd_disable(args: argparse.Namespace, root: pathlib.Path) -> None:
597 """Remove the sparse-checkout config.
598
599 Idempotent — no error if already disabled. The next ``checkout`` or
600 ``merge`` will materialise the full working tree.
601
602 Agent quickstart::
603
604 muse sparse-checkout disable --json
605
606 JSON fields::
607
608 was_enabled ``true`` when sparse-checkout was active before this command.
609 muse_version Muse release that produced this output.
610 schema Envelope schema version (int).
611 exit_code ``0`` on success.
612 duration_ms Wall-clock milliseconds for the command.
613 timestamp ISO-8601 UTC timestamp of command completion.
614 warnings List of non-fatal advisory messages.
615
616 Exit codes::
617
618 0 Success (including when sparse-checkout was already disabled).
619 1 User error (corrupt config).
620 """
621 elapsed = start_timer()
622
623 json_out: bool = getattr(args, "json_out", False)
624 cfg = _read_config_safe(root)
625 was_enabled = cfg is not None
626
627 if was_enabled:
628 remove_sparse_config(root)
629
630 if json_out:
631 print(_json.dumps(_SparseDisableJson(
632 **make_envelope(elapsed),
633 was_enabled=was_enabled,
634 )))
635 else:
636 if was_enabled:
637 print("Sparse-checkout disabled. Full working tree will be restored on next checkout.")
638 else:
639 print("Sparse-checkout is already disabled.")
640
641 # ---------------------------------------------------------------------------
642 # Registration
643 # ---------------------------------------------------------------------------
644
645 def register(
646 subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]",
647 ) -> None:
648 """Register the ``muse sparse-checkout`` subcommand."""
649 parser = subparsers.add_parser(
650 "sparse-checkout",
651 help="Partial working-tree materialization.",
652 description=__doc__,
653 formatter_class=argparse.RawDescriptionHelpFormatter,
654 )
655 sub = parser.add_subparsers(dest="sc_command", metavar="SUBCOMMAND")
656 sub.required = True
657
658 # init
659 p_init = sub.add_parser(
660 "init",
661 help="Activate sparse-checkout (or switch mode on existing config).",
662 )
663 p_init.add_argument(
664 "--no-cone",
665 action="store_true",
666 default=False,
667 help="Use glob-pattern mode instead of cone (directory-prefix) mode.",
668 )
669 p_init.add_argument(
670 "--json", "-j",
671 action="store_true",
672 dest="json_out",
673 help="Emit machine-readable JSON output.",
674 )
675 p_init.set_defaults(sc_func=_cmd_init)
676
677 # set
678 p_set = sub.add_parser("set", help="Replace pattern list.")
679 p_set.add_argument("patterns", nargs="+", metavar="PATTERN")
680 p_set.add_argument(
681 "--json", "-j",
682 action="store_true",
683 dest="json_out",
684 help="Emit machine-readable JSON output.",
685 )
686 p_set.set_defaults(sc_func=_cmd_set)
687
688 # add
689 p_add = sub.add_parser("add", help="Append patterns.")
690 p_add.add_argument("patterns", nargs="+", metavar="PATTERN")
691 p_add.add_argument(
692 "--json", "-j",
693 action="store_true",
694 dest="json_out",
695 help="Emit machine-readable JSON output.",
696 )
697 p_add.set_defaults(sc_func=_cmd_add)
698
699 # list
700 p_list = sub.add_parser("list", help="Show active patterns.")
701 p_list.add_argument(
702 "--json", "-j",
703 action="store_true",
704 dest="json_out",
705 help="Emit machine-readable JSON output.",
706 )
707 p_list.set_defaults(sc_func=_cmd_list)
708
709 # stats
710 p_stats = sub.add_parser(
711 "stats",
712 help="Show how many HEAD-snapshot files match the sparse config.",
713 )
714 p_stats.add_argument(
715 "--json", "-j",
716 action="store_true",
717 dest="json_out",
718 help="Emit machine-readable JSON output (default for stats).",
719 )
720 p_stats.set_defaults(sc_func=_cmd_stats)
721
722 # disable
723 p_dis = sub.add_parser("disable", help="Remove sparse-checkout configuration.")
724 p_dis.add_argument(
725 "--json", "-j",
726 action="store_true",
727 dest="json_out",
728 help="Emit machine-readable JSON output.",
729 )
730 p_dis.set_defaults(sc_func=_cmd_disable)
731
732 parser.set_defaults(func=run)
733
734 # ---------------------------------------------------------------------------
735 # Entry point
736 # ---------------------------------------------------------------------------
737
738 def run(args: argparse.Namespace) -> None:
739 """Dispatch ``muse sparse-checkout`` to the appropriate subcommand handler.
740
741 Subcommands: ``init``, ``set``, ``add``, ``list``, ``stats``, ``disable``.
742 Each handler carries its own docstring, JSON schema, and exit codes.
743 """
744 root = require_repo()
745 args.sc_func(args, root)
File History 1 commit
sha256:84df9126d09aeec0b8f1b908f0b06c10913feec28f3514b382efb1ba6d619385 refactor: rename StructuredMergePlugin to AddressedMergePlu… Sonnet 4.6 minor 24 days ago