gitwise-cli 0.24.2__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 (125) hide show
  1. gitwise/__init__.py +11 -0
  2. gitwise/__main__.py +113 -0
  3. gitwise/_cli_completions.py +88 -0
  4. gitwise/_cli_dispatch.py +469 -0
  5. gitwise/_cli_introspection.py +275 -0
  6. gitwise/_cli_parser.py +345 -0
  7. gitwise/_cli_setup_agents.py +439 -0
  8. gitwise/_i18n_data.json +1934 -0
  9. gitwise/_paths.py +22 -0
  10. gitwise/_runtime_config.py +246 -0
  11. gitwise/audit.py +338 -0
  12. gitwise/branches.py +183 -0
  13. gitwise/clean.py +197 -0
  14. gitwise/commit.py +142 -0
  15. gitwise/conflicts.py +112 -0
  16. gitwise/context.py +163 -0
  17. gitwise/design.py +383 -0
  18. gitwise/diff.py +309 -0
  19. gitwise/doctor.py +116 -0
  20. gitwise/git.py +254 -0
  21. gitwise/health.py +345 -0
  22. gitwise/i18n.py +99 -0
  23. gitwise/log.py +329 -0
  24. gitwise/merge.py +193 -0
  25. gitwise/optimize.py +212 -0
  26. gitwise/output.py +652 -0
  27. gitwise/pick.py +102 -0
  28. gitwise/pr.py +543 -0
  29. gitwise/py.typed +0 -0
  30. gitwise/schema.py +49 -0
  31. gitwise/setup.py +551 -0
  32. gitwise/setup_agents/__init__.py +36 -0
  33. gitwise/setup_agents/adapters/__init__.py +17 -0
  34. gitwise/setup_agents/adapters/aider.py +5 -0
  35. gitwise/setup_agents/adapters/base.py +5 -0
  36. gitwise/setup_agents/adapters/codex.py +5 -0
  37. gitwise/setup_agents/adapters/continue_adapter.py +5 -0
  38. gitwise/setup_agents/adapters/cursor.py +5 -0
  39. gitwise/setup_agents/adapters/opencode.py +5 -0
  40. gitwise/setup_agents/adapters/pi.py +5 -0
  41. gitwise/setup_agents/exec.py +449 -0
  42. gitwise/setup_agents/format.py +164 -0
  43. gitwise/setup_agents/plan.py +254 -0
  44. gitwise/setup_agents/plan_gitfiles.py +167 -0
  45. gitwise/setup_agents/plan_skills.py +256 -0
  46. gitwise/setup_agents/providers/__init__.py +96 -0
  47. gitwise/setup_agents/providers/aider.py +11 -0
  48. gitwise/setup_agents/providers/base.py +79 -0
  49. gitwise/setup_agents/providers/claude.py +408 -0
  50. gitwise/setup_agents/providers/codex.py +11 -0
  51. gitwise/setup_agents/providers/continue_adapter.py +11 -0
  52. gitwise/setup_agents/providers/cursor.py +11 -0
  53. gitwise/setup_agents/providers/opencode.py +11 -0
  54. gitwise/setup_agents/providers/pi.py +11 -0
  55. gitwise/setup_agents/state.py +141 -0
  56. gitwise/setup_agents/types.py +48 -0
  57. gitwise/share/agents/skills/git-audit/SKILL.md +25 -0
  58. gitwise/share/agents/skills/git-clean/SKILL.md +22 -0
  59. gitwise/share/agents/skills/git-optimize/SKILL.md +21 -0
  60. gitwise/share/aider/CONVENTIONS.md.template +8 -0
  61. gitwise/share/aider/aider.conf.yml.template +4 -0
  62. gitwise/share/claude/CLAUDE.md.template +9 -0
  63. gitwise/share/claude/rules/gitwise.md +16 -0
  64. gitwise/share/claude/settings.json.template +47 -0
  65. gitwise/share/claude/skills/git-audit/SKILL.md +25 -0
  66. gitwise/share/claude/skills/git-clean/SKILL.md +22 -0
  67. gitwise/share/claude/skills/git-optimize/SKILL.md +21 -0
  68. gitwise/share/codex/agents/gitwise.toml.template +18 -0
  69. gitwise/share/continue/rules/gitwise.md.template +14 -0
  70. gitwise/share/cursor/rules/gitwise.mdc.template +16 -0
  71. gitwise/share/git-config-modern.txt +48 -0
  72. gitwise/share/hooks/commit-msg +22 -0
  73. gitwise/share/hooks/pre-commit +19 -0
  74. gitwise/share/opencode/agents/gitwise.md.template +14 -0
  75. gitwise/share/pi/skills/gitwise.md.template +14 -0
  76. gitwise/share/schemas/v1/input/audit.json +40 -0
  77. gitwise/share/schemas/v1/input/branches.json +51 -0
  78. gitwise/share/schemas/v1/input/clean.json +52 -0
  79. gitwise/share/schemas/v1/input/commands.json +36 -0
  80. gitwise/share/schemas/v1/input/commit.json +63 -0
  81. gitwise/share/schemas/v1/input/completions.json +51 -0
  82. gitwise/share/schemas/v1/input/conflicts.json +46 -0
  83. gitwise/share/schemas/v1/input/context.json +36 -0
  84. gitwise/share/schemas/v1/input/diff.json +56 -0
  85. gitwise/share/schemas/v1/input/doctor.json +36 -0
  86. gitwise/share/schemas/v1/input/health.json +36 -0
  87. gitwise/share/schemas/v1/input/log.json +71 -0
  88. gitwise/share/schemas/v1/input/merge.json +63 -0
  89. gitwise/share/schemas/v1/input/optimize.json +44 -0
  90. gitwise/share/schemas/v1/input/pick.json +63 -0
  91. gitwise/share/schemas/v1/input/pr.json +51 -0
  92. gitwise/share/schemas/v1/input/schema.json +48 -0
  93. gitwise/share/schemas/v1/input/setup-agents.json +108 -0
  94. gitwise/share/schemas/v1/input/setup.json +55 -0
  95. gitwise/share/schemas/v1/input/show.json +46 -0
  96. gitwise/share/schemas/v1/input/snapshot.json +36 -0
  97. gitwise/share/schemas/v1/input/stash.json +68 -0
  98. gitwise/share/schemas/v1/input/status.json +36 -0
  99. gitwise/share/schemas/v1/input/suggest.json +36 -0
  100. gitwise/share/schemas/v1/input/summarize.json +44 -0
  101. gitwise/share/schemas/v1/input/sync.json +55 -0
  102. gitwise/share/schemas/v1/input/tag.json +73 -0
  103. gitwise/share/schemas/v1/input/undo.json +60 -0
  104. gitwise/share/schemas/v1/input/update.json +40 -0
  105. gitwise/share/schemas/v1/input/worktree.json +50 -0
  106. gitwise/show.py +118 -0
  107. gitwise/snapshot.py +110 -0
  108. gitwise/stash.py +188 -0
  109. gitwise/status.py +93 -0
  110. gitwise/suggest.py +148 -0
  111. gitwise/summarize.py +202 -0
  112. gitwise/sync.py +257 -0
  113. gitwise/tag.py +252 -0
  114. gitwise/undo.py +145 -0
  115. gitwise/update.py +42 -0
  116. gitwise/utils/__init__.py +1 -0
  117. gitwise/utils/git_output.py +51 -0
  118. gitwise/utils/json_envelope.py +58 -0
  119. gitwise/utils/parsing.py +34 -0
  120. gitwise/worktree.py +182 -0
  121. gitwise_cli-0.24.2.dist-info/METADATA +151 -0
  122. gitwise_cli-0.24.2.dist-info/RECORD +125 -0
  123. gitwise_cli-0.24.2.dist-info/WHEEL +4 -0
  124. gitwise_cli-0.24.2.dist-info/entry_points.txt +2 -0
  125. gitwise_cli-0.24.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://gitwise.dev/schemas/v1/input/tag.json",
4
+ "title": "gitwise tag cli input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "lang": {
9
+ "type": "string",
10
+ "enum": [
11
+ "es",
12
+ "en"
13
+ ],
14
+ "description": "output language (default: auto-detect from locale)"
15
+ },
16
+ "theme": {
17
+ "type": "string",
18
+ "enum": [
19
+ "dark",
20
+ "light",
21
+ "auto"
22
+ ],
23
+ "description": "color theme: dark, light, or auto-detect (default: auto)"
24
+ },
25
+ "json": {
26
+ "type": "boolean",
27
+ "description": "output JSON",
28
+ "default": false
29
+ },
30
+ "json_pretty": {
31
+ "type": "boolean",
32
+ "description": "pretty-print JSON output",
33
+ "default": false
34
+ },
35
+ "action": {
36
+ "type": "string",
37
+ "enum": [
38
+ "list",
39
+ "latest",
40
+ "create",
41
+ "delete"
42
+ ],
43
+ "default": "list"
44
+ },
45
+ "name": {
46
+ "type": "string",
47
+ "description": "tag name (for create/delete)"
48
+ },
49
+ "bump": {
50
+ "type": "string",
51
+ "enum": [
52
+ "major",
53
+ "minor",
54
+ "patch"
55
+ ],
56
+ "description": "bump semver part"
57
+ },
58
+ "message": {
59
+ "type": "string",
60
+ "description": "annotated tag message"
61
+ },
62
+ "dry_run": {
63
+ "type": "boolean",
64
+ "description": "show without executing",
65
+ "default": false
66
+ },
67
+ "yes": {
68
+ "type": "boolean",
69
+ "description": "skip confirmation",
70
+ "default": false
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://gitwise.dev/schemas/v1/input/undo.json",
4
+ "title": "gitwise undo cli input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "lang": {
9
+ "type": "string",
10
+ "enum": [
11
+ "es",
12
+ "en"
13
+ ],
14
+ "description": "output language (default: auto-detect from locale)"
15
+ },
16
+ "theme": {
17
+ "type": "string",
18
+ "enum": [
19
+ "dark",
20
+ "light",
21
+ "auto"
22
+ ],
23
+ "description": "color theme: dark, light, or auto-detect (default: auto)"
24
+ },
25
+ "json": {
26
+ "type": "boolean",
27
+ "description": "output JSON",
28
+ "default": false
29
+ },
30
+ "json_pretty": {
31
+ "type": "boolean",
32
+ "description": "pretty-print JSON output",
33
+ "default": false
34
+ },
35
+ "ref": {
36
+ "type": "string",
37
+ "description": "target ref (default: HEAD~1)"
38
+ },
39
+ "soft": {
40
+ "type": "boolean",
41
+ "description": "soft reset (keep working tree)",
42
+ "default": false
43
+ },
44
+ "steps": {
45
+ "type": "integer",
46
+ "description": "number of steps back",
47
+ "default": 1
48
+ },
49
+ "dry_run": {
50
+ "type": "boolean",
51
+ "description": "show without resetting",
52
+ "default": false
53
+ },
54
+ "yes": {
55
+ "type": "boolean",
56
+ "description": "skip confirmation for --hard",
57
+ "default": false
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://gitwise.dev/schemas/v1/input/update.json",
4
+ "title": "gitwise update cli input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "lang": {
9
+ "type": "string",
10
+ "enum": [
11
+ "es",
12
+ "en"
13
+ ],
14
+ "description": "output language (default: auto-detect from locale)"
15
+ },
16
+ "theme": {
17
+ "type": "string",
18
+ "enum": [
19
+ "dark",
20
+ "light",
21
+ "auto"
22
+ ],
23
+ "description": "color theme: dark, light, or auto-detect (default: auto)"
24
+ },
25
+ "json": {
26
+ "type": "boolean",
27
+ "description": "output JSON",
28
+ "default": false
29
+ },
30
+ "json_pretty": {
31
+ "type": "boolean",
32
+ "description": "pretty-print JSON output",
33
+ "default": false
34
+ },
35
+ "dry_run": {
36
+ "type": "boolean",
37
+ "default": false
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://gitwise.dev/schemas/v1/input/worktree.json",
4
+ "title": "gitwise worktree cli input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "lang": {
9
+ "type": "string",
10
+ "enum": [
11
+ "es",
12
+ "en"
13
+ ],
14
+ "description": "output language (default: auto-detect from locale)"
15
+ },
16
+ "theme": {
17
+ "type": "string",
18
+ "enum": [
19
+ "dark",
20
+ "light",
21
+ "auto"
22
+ ],
23
+ "description": "color theme: dark, light, or auto-detect (default: auto)"
24
+ },
25
+ "json": {
26
+ "type": "boolean",
27
+ "description": "output JSON",
28
+ "default": false
29
+ },
30
+ "json_pretty": {
31
+ "type": "boolean",
32
+ "description": "pretty-print JSON output",
33
+ "default": false
34
+ },
35
+ "action": {
36
+ "type": "string",
37
+ "enum": [
38
+ "new",
39
+ "clean"
40
+ ]
41
+ },
42
+ "branch": {
43
+ "type": "string"
44
+ },
45
+ "dry_run": {
46
+ "type": "boolean",
47
+ "default": false
48
+ }
49
+ }
50
+ }
gitwise/show.py ADDED
@@ -0,0 +1,118 @@
1
+ """gitwise show — commit inspector with stat and JSON output."""
2
+
3
+ from .git import require_root, validate_ref
4
+ from .git import run as git_run
5
+ from .i18n import t
6
+ from .output import bat_pipe, error, print_diffstat, print_header, print_json
7
+ from .utils.git_output import parse_diffstat_entries, parse_name_status_entries
8
+ from .utils.json_envelope import ok_envelope
9
+
10
+
11
+ def _build_show_args(ref: str = "HEAD", stat: bool = False) -> list[str]:
12
+ args = ["show"]
13
+ if stat:
14
+ args.append("--stat")
15
+ else:
16
+ args.append(
17
+ "--format=%C(yellow)%H%C(reset)%n%C(dim)%ad%C(reset) %C(bold)%an%C(reset) <%ae>%n%C(dim)%d%C(reset)%n%n %s%n"
18
+ )
19
+ args.append("--patch")
20
+ args.append(ref)
21
+ return args
22
+
23
+
24
+ def _parse_diffstat_entries(raw: str) -> list[dict[str, str]]:
25
+ return parse_diffstat_entries(raw)
26
+
27
+
28
+ def _show_status_map(root, ref: str) -> dict[str, str]:
29
+ r = git_run(["show", "--name-status", "--format=", ref], cwd=root, check=False)
30
+ if r.returncode != 0:
31
+ return {}
32
+ status_map: dict[str, str] = {}
33
+ for item in parse_name_status_entries(r.stdout):
34
+ status = str(item.get("status") or "")[:1].upper()
35
+ path = str(item.get("path") or "").strip()
36
+ if path:
37
+ status_map[path] = status
38
+ return status_map
39
+
40
+
41
+ def _build_show_json_args(ref: str = "HEAD") -> list[str]:
42
+ return [
43
+ "show",
44
+ "--format=%H%n%h%n%an%n%ae%n%ad%n%s",
45
+ "-s",
46
+ ref,
47
+ ]
48
+
49
+
50
+ def _parse_show_json(raw: str) -> dict[str, str | list[str] | int | bool]:
51
+ lines = [ln for ln in raw.strip().splitlines() if ln.strip()]
52
+ if len(lines) >= 6:
53
+ return {
54
+ "hash": lines[0],
55
+ "short_hash": lines[1],
56
+ "author": lines[2],
57
+ "email": lines[3],
58
+ "date": lines[4],
59
+ "subject": lines[5],
60
+ }
61
+ return {"raw": raw}
62
+
63
+
64
+ def run_show(
65
+ *,
66
+ ref: str = "HEAD",
67
+ stat: bool = False,
68
+ as_json: bool = False,
69
+ ) -> int:
70
+ root, err = require_root()
71
+ if err:
72
+ return err
73
+ if root is None:
74
+ return 1
75
+
76
+ if not validate_ref(ref):
77
+ error(t("invalid_ref", ref=ref))
78
+ return 1
79
+
80
+ if as_json:
81
+ args = _build_show_json_args(ref)
82
+ r = git_run(args, cwd=root, check=False)
83
+ if r.returncode != 0:
84
+ error(t("git_show_failed", error=r.stderr.strip()))
85
+ return 1
86
+ data = _parse_show_json(r.stdout)
87
+ print_json(ok_envelope(payload=data))
88
+ else:
89
+ if stat:
90
+ r = git_run(["show", "--stat", "--format=", ref], cwd=root, check=False)
91
+ if r.returncode != 0:
92
+ error(t("git_show_failed", error=r.stderr.strip()))
93
+ return 1
94
+ entries = _parse_diffstat_entries(r.stdout)
95
+ if entries:
96
+ status_map = _show_status_map(root, ref)
97
+ styled_entries = [
98
+ {
99
+ "path": entry["path"],
100
+ "changes": entry["changes"],
101
+ "status": status_map.get(entry["path"], "M"),
102
+ }
103
+ for entry in entries
104
+ ]
105
+ print_diffstat(t("show_header", ref=ref), styled_entries)
106
+ else:
107
+ print_header(t("show_header", ref=ref))
108
+ bat_pipe(r.stdout, language="diff")
109
+ else:
110
+ args = _build_show_args(ref, stat)
111
+ r = git_run(args, cwd=root, check=False)
112
+ if r.returncode != 0:
113
+ error(t("git_show_failed", error=r.stderr.strip()))
114
+ return 1
115
+ print_header(t("show_header", ref=ref))
116
+ bat_pipe(r.stdout, language="diff")
117
+
118
+ return 0
gitwise/snapshot.py ADDED
@@ -0,0 +1,110 @@
1
+ """Generates git snapshot file for session context."""
2
+
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+
6
+ from .git import require_root
7
+ from .git import run as git_run
8
+ from .i18n import t
9
+ from .output import debug, print_header, print_json
10
+ from .utils.json_envelope import ok_envelope
11
+
12
+
13
+ def _append_branch_section(lines: list[str], *, root: Path) -> None:
14
+ branch = git_run(["branch", "--show-current"], cwd=root, check=False)
15
+ if branch.returncode != 0:
16
+ return
17
+ lines += [
18
+ t("section_current_branch"),
19
+ "```",
20
+ branch.stdout.strip() or "(detached HEAD)",
21
+ "```",
22
+ "",
23
+ ]
24
+
25
+
26
+ def _append_status_section(lines: list[str], *, root: Path) -> None:
27
+ status = git_run(["status", "--short"], cwd=root, check=False)
28
+ if status.returncode != 0:
29
+ return
30
+ lines += [
31
+ t("section_status"),
32
+ "```",
33
+ status.stdout.strip() or t("status_clean"),
34
+ "```",
35
+ "",
36
+ ]
37
+
38
+
39
+ def _append_log_section(lines: list[str], *, root: Path) -> None:
40
+ log = git_run(["--no-pager", "log", "--oneline", "-n", "10"], cwd=root, check=False)
41
+ if log.returncode == 0 and log.stdout.strip():
42
+ lines += [t("section_last_commits"), "```", log.stdout.strip(), "```", ""]
43
+
44
+
45
+ def _append_stash_section(lines: list[str], *, root: Path) -> None:
46
+ stash = git_run(["stash", "list"], cwd=root, check=False)
47
+ if stash.returncode != 0 or not stash.stdout.strip():
48
+ return
49
+ stash_count = len(stash.stdout.strip().splitlines())
50
+ lines += [t("stashes_section", count=str(stash_count)), ""]
51
+
52
+
53
+ def _append_worktrees_section(lines: list[str], *, root: Path) -> None:
54
+ worktrees = git_run(["worktree", "list", "--porcelain"], cwd=root, check=False)
55
+ if worktrees.returncode != 0:
56
+ return
57
+ wt_count = worktrees.stdout.count("worktree ")
58
+ if wt_count > 1:
59
+ lines += [t("worktrees_active", count=str(wt_count)), ""]
60
+
61
+
62
+ def generate_snapshot(
63
+ root: Path,
64
+ *,
65
+ frozen_time: bool = False,
66
+ relative_path: str = ".claude/git-snapshot.md",
67
+ ) -> Path:
68
+ """Write snapshot markdown in repo root. Updates generated_at on every call."""
69
+ snapshot_path = root / Path(relative_path)
70
+ snapshot_path.parent.mkdir(parents=True, exist_ok=True)
71
+
72
+ if frozen_time:
73
+ generated_at = "1970-01-01T00:00:00Z"
74
+ else:
75
+ generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
76
+
77
+ lines: list[str] = ["# Git Snapshot", "", f"generated_at: {generated_at}", ""]
78
+
79
+ _append_branch_section(lines, root=root)
80
+ _append_status_section(lines, root=root)
81
+ _append_log_section(lines, root=root)
82
+ _append_stash_section(lines, root=root)
83
+ _append_worktrees_section(lines, root=root)
84
+
85
+ tmp = snapshot_path.with_suffix(".tmp")
86
+ try:
87
+ tmp.write_text("\n".join(lines) + "\n", encoding="utf-8")
88
+ tmp.replace(snapshot_path)
89
+ except BaseException:
90
+ tmp.unlink(missing_ok=True)
91
+ raise
92
+ debug(t("debug_snapshot_written", path=str(snapshot_path)))
93
+ return snapshot_path
94
+
95
+
96
+ def run_snapshot(*, as_json: bool = False) -> int:
97
+ root, err = require_root()
98
+ if err:
99
+ return err
100
+ if root is None:
101
+ return 1
102
+
103
+ path = generate_snapshot(root)
104
+
105
+ if as_json:
106
+ print_json(ok_envelope(path=str(path)))
107
+ return 0
108
+
109
+ print_header(t("snapshot_generated", path=str(path.relative_to(root))))
110
+ return 0
gitwise/stash.py ADDED
@@ -0,0 +1,188 @@
1
+ """gitwise stash — manage stashes by index or age (list/show/pop/drop/clear)."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .git import require_root
6
+ from .git import run as git_run
7
+ from .i18n import t
8
+ from .output import (
9
+ confirm,
10
+ error,
11
+ info,
12
+ ok,
13
+ print_diffstat,
14
+ print_header,
15
+ print_json,
16
+ print_table,
17
+ warn,
18
+ )
19
+ from .utils.git_output import parse_diffstat_entries
20
+ from .utils.json_envelope import error_envelope, ok_envelope
21
+
22
+
23
+ def _parse_diffstat_entries(raw: str) -> list[dict[str, str]]:
24
+ return parse_diffstat_entries(raw, default_status="M")
25
+
26
+
27
+ def _stash_list(root: Path) -> list[dict[str, str]]:
28
+ r = git_run(["stash", "list"], cwd=root, check=False)
29
+ if r.returncode != 0 or not r.stdout.strip():
30
+ return []
31
+ result: list[dict[str, str]] = []
32
+ for line in r.stdout.splitlines():
33
+ parts = line.split(": ", 2)
34
+ entry: dict[str, str] = {"ref": parts[0]}
35
+ if len(parts) >= 2:
36
+ entry["branch"] = parts[1].strip()
37
+ if len(parts) >= 3:
38
+ entry["message"] = parts[2].strip()
39
+ result.append(entry)
40
+ return result
41
+
42
+
43
+ def _cmd_list(root: Path, *, as_json: bool) -> int:
44
+ stashes = _stash_list(root)
45
+ if as_json:
46
+ print_json(ok_envelope(stashes=stashes, count=len(stashes)))
47
+ return 0
48
+ if not stashes:
49
+ ok(t("stash_empty"))
50
+ return 0
51
+
52
+ columns = [
53
+ (t("stash_col_ref"), "ref"),
54
+ (t("stash_col_branch"), "branch"),
55
+ (t("stash_col_message"), "message"),
56
+ ]
57
+
58
+ rows: list[list[str]] = []
59
+ for s in stashes:
60
+ rows.append(
61
+ [
62
+ s.get("ref", ""),
63
+ s.get("branch", ""),
64
+ s.get("message", ""),
65
+ ]
66
+ )
67
+
68
+ print_table(
69
+ title=t("stash_list_title"),
70
+ columns=columns,
71
+ rows=rows,
72
+ )
73
+ return 0
74
+
75
+
76
+ def _cmd_show(root: Path, index: int, *, as_json: bool, patch: bool = False) -> int:
77
+ ref = f"stash@{{{index}}}"
78
+ stat_args = ["stash", "show", "--stat", ref]
79
+ if patch:
80
+ stat_args = ["stash", "show", "-p", ref]
81
+ r = git_run(stat_args, cwd=root, check=False)
82
+ if r.returncode != 0:
83
+ msg = t("stash_not_found", index=str(index))
84
+ if as_json:
85
+ print_json(error_envelope(error=msg, code="stash_not_found", hint=t("stash_hint")))
86
+ return 1
87
+ error(msg, hint=t("stash_hint"))
88
+ return 1
89
+ if as_json:
90
+ print_json(ok_envelope(ref=ref, stat=r.stdout.strip()))
91
+ return 0
92
+ if patch:
93
+ print_header(ref)
94
+ for line in r.stdout.strip().splitlines():
95
+ info(line)
96
+ return 0
97
+
98
+ entries = _parse_diffstat_entries(r.stdout)
99
+ if entries:
100
+ print_diffstat(ref, entries)
101
+ else:
102
+ print_header(ref)
103
+ for line in r.stdout.strip().splitlines():
104
+ info(line)
105
+ return 0
106
+
107
+
108
+ def _cmd_pop(root: Path, index: int, *, as_json: bool) -> int:
109
+ ref = f"stash@{{{index}}}"
110
+ r = git_run(["stash", "pop", ref], cwd=root, check=False)
111
+ if r.returncode != 0:
112
+ error(r.stderr.strip())
113
+ return 1
114
+ if as_json:
115
+ print_json(ok_envelope(popped=ref))
116
+ return 0
117
+ ok(t("stash_popped", ref=ref))
118
+ return 0
119
+
120
+
121
+ def _cmd_drop(root: Path, index: int, *, as_json: bool, yes: bool = False) -> int:
122
+ ref = f"stash@{{{index}}}"
123
+ if not yes and not confirm(t("confirm_stash_drop", ref=ref)):
124
+ warn(t("aborted"))
125
+ return 1
126
+ r = git_run(["stash", "drop", ref], cwd=root, check=False)
127
+ if r.returncode != 0:
128
+ error(r.stderr.strip())
129
+ return 1
130
+ if as_json:
131
+ print_json(ok_envelope(dropped=ref))
132
+ return 0
133
+ ok(t("stash_dropped", ref=ref))
134
+ return 0
135
+
136
+
137
+ def _cmd_clean(root: Path, *, as_json: bool, yes: bool = False, dry_run: bool = False) -> int:
138
+ stashes = _stash_list(root)
139
+ if not stashes:
140
+ ok(t("stash_empty"))
141
+ return 0
142
+ if dry_run:
143
+ if as_json:
144
+ print_json(ok_envelope(would_drop=len(stashes), dry_run=True))
145
+ return 0
146
+ ok(t("stash_clean_dry", count=str(len(stashes))))
147
+ return 0
148
+ if not yes and not confirm(t("confirm_stash_clean", count=str(len(stashes)))):
149
+ warn(t("aborted"))
150
+ return 1
151
+ r = git_run(["stash", "clear"], cwd=root, check=False)
152
+ if r.returncode != 0:
153
+ error(r.stderr.strip())
154
+ return 1
155
+ if as_json:
156
+ print_json(ok_envelope(cleared=len(stashes)))
157
+ return 0
158
+ ok(t("stash_cleaned", count=str(len(stashes))))
159
+ return 0
160
+
161
+
162
+ def run_stash(
163
+ action: str = "list",
164
+ index: int = 0,
165
+ *,
166
+ as_json: bool = False,
167
+ yes: bool = False,
168
+ dry_run: bool = False,
169
+ patch: bool = False,
170
+ ) -> int:
171
+ root, err = require_root()
172
+ if err:
173
+ return err
174
+ if root is None:
175
+ return 1
176
+
177
+ if action == "list":
178
+ return _cmd_list(root, as_json=as_json)
179
+ if action == "show":
180
+ return _cmd_show(root, index, as_json=as_json, patch=patch)
181
+ if action == "pop":
182
+ return _cmd_pop(root, index, as_json=as_json)
183
+ if action == "drop":
184
+ return _cmd_drop(root, index, as_json=as_json, yes=yes)
185
+ if action in ("clean", "clear"):
186
+ return _cmd_clean(root, as_json=as_json, yes=yes, dry_run=dry_run)
187
+ error(t("stash_unknown_action", action=action))
188
+ return 1