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.
- gitwise/__init__.py +11 -0
- gitwise/__main__.py +113 -0
- gitwise/_cli_completions.py +88 -0
- gitwise/_cli_dispatch.py +469 -0
- gitwise/_cli_introspection.py +275 -0
- gitwise/_cli_parser.py +345 -0
- gitwise/_cli_setup_agents.py +439 -0
- gitwise/_i18n_data.json +1934 -0
- gitwise/_paths.py +22 -0
- gitwise/_runtime_config.py +246 -0
- gitwise/audit.py +338 -0
- gitwise/branches.py +183 -0
- gitwise/clean.py +197 -0
- gitwise/commit.py +142 -0
- gitwise/conflicts.py +112 -0
- gitwise/context.py +163 -0
- gitwise/design.py +383 -0
- gitwise/diff.py +309 -0
- gitwise/doctor.py +116 -0
- gitwise/git.py +254 -0
- gitwise/health.py +345 -0
- gitwise/i18n.py +99 -0
- gitwise/log.py +329 -0
- gitwise/merge.py +193 -0
- gitwise/optimize.py +212 -0
- gitwise/output.py +652 -0
- gitwise/pick.py +102 -0
- gitwise/pr.py +543 -0
- gitwise/py.typed +0 -0
- gitwise/schema.py +49 -0
- gitwise/setup.py +551 -0
- gitwise/setup_agents/__init__.py +36 -0
- gitwise/setup_agents/adapters/__init__.py +17 -0
- gitwise/setup_agents/adapters/aider.py +5 -0
- gitwise/setup_agents/adapters/base.py +5 -0
- gitwise/setup_agents/adapters/codex.py +5 -0
- gitwise/setup_agents/adapters/continue_adapter.py +5 -0
- gitwise/setup_agents/adapters/cursor.py +5 -0
- gitwise/setup_agents/adapters/opencode.py +5 -0
- gitwise/setup_agents/adapters/pi.py +5 -0
- gitwise/setup_agents/exec.py +449 -0
- gitwise/setup_agents/format.py +164 -0
- gitwise/setup_agents/plan.py +254 -0
- gitwise/setup_agents/plan_gitfiles.py +167 -0
- gitwise/setup_agents/plan_skills.py +256 -0
- gitwise/setup_agents/providers/__init__.py +96 -0
- gitwise/setup_agents/providers/aider.py +11 -0
- gitwise/setup_agents/providers/base.py +79 -0
- gitwise/setup_agents/providers/claude.py +408 -0
- gitwise/setup_agents/providers/codex.py +11 -0
- gitwise/setup_agents/providers/continue_adapter.py +11 -0
- gitwise/setup_agents/providers/cursor.py +11 -0
- gitwise/setup_agents/providers/opencode.py +11 -0
- gitwise/setup_agents/providers/pi.py +11 -0
- gitwise/setup_agents/state.py +141 -0
- gitwise/setup_agents/types.py +48 -0
- gitwise/share/agents/skills/git-audit/SKILL.md +25 -0
- gitwise/share/agents/skills/git-clean/SKILL.md +22 -0
- gitwise/share/agents/skills/git-optimize/SKILL.md +21 -0
- gitwise/share/aider/CONVENTIONS.md.template +8 -0
- gitwise/share/aider/aider.conf.yml.template +4 -0
- gitwise/share/claude/CLAUDE.md.template +9 -0
- gitwise/share/claude/rules/gitwise.md +16 -0
- gitwise/share/claude/settings.json.template +47 -0
- gitwise/share/claude/skills/git-audit/SKILL.md +25 -0
- gitwise/share/claude/skills/git-clean/SKILL.md +22 -0
- gitwise/share/claude/skills/git-optimize/SKILL.md +21 -0
- gitwise/share/codex/agents/gitwise.toml.template +18 -0
- gitwise/share/continue/rules/gitwise.md.template +14 -0
- gitwise/share/cursor/rules/gitwise.mdc.template +16 -0
- gitwise/share/git-config-modern.txt +48 -0
- gitwise/share/hooks/commit-msg +22 -0
- gitwise/share/hooks/pre-commit +19 -0
- gitwise/share/opencode/agents/gitwise.md.template +14 -0
- gitwise/share/pi/skills/gitwise.md.template +14 -0
- gitwise/share/schemas/v1/input/audit.json +40 -0
- gitwise/share/schemas/v1/input/branches.json +51 -0
- gitwise/share/schemas/v1/input/clean.json +52 -0
- gitwise/share/schemas/v1/input/commands.json +36 -0
- gitwise/share/schemas/v1/input/commit.json +63 -0
- gitwise/share/schemas/v1/input/completions.json +51 -0
- gitwise/share/schemas/v1/input/conflicts.json +46 -0
- gitwise/share/schemas/v1/input/context.json +36 -0
- gitwise/share/schemas/v1/input/diff.json +56 -0
- gitwise/share/schemas/v1/input/doctor.json +36 -0
- gitwise/share/schemas/v1/input/health.json +36 -0
- gitwise/share/schemas/v1/input/log.json +71 -0
- gitwise/share/schemas/v1/input/merge.json +63 -0
- gitwise/share/schemas/v1/input/optimize.json +44 -0
- gitwise/share/schemas/v1/input/pick.json +63 -0
- gitwise/share/schemas/v1/input/pr.json +51 -0
- gitwise/share/schemas/v1/input/schema.json +48 -0
- gitwise/share/schemas/v1/input/setup-agents.json +108 -0
- gitwise/share/schemas/v1/input/setup.json +55 -0
- gitwise/share/schemas/v1/input/show.json +46 -0
- gitwise/share/schemas/v1/input/snapshot.json +36 -0
- gitwise/share/schemas/v1/input/stash.json +68 -0
- gitwise/share/schemas/v1/input/status.json +36 -0
- gitwise/share/schemas/v1/input/suggest.json +36 -0
- gitwise/share/schemas/v1/input/summarize.json +44 -0
- gitwise/share/schemas/v1/input/sync.json +55 -0
- gitwise/share/schemas/v1/input/tag.json +73 -0
- gitwise/share/schemas/v1/input/undo.json +60 -0
- gitwise/share/schemas/v1/input/update.json +40 -0
- gitwise/share/schemas/v1/input/worktree.json +50 -0
- gitwise/show.py +118 -0
- gitwise/snapshot.py +110 -0
- gitwise/stash.py +188 -0
- gitwise/status.py +93 -0
- gitwise/suggest.py +148 -0
- gitwise/summarize.py +202 -0
- gitwise/sync.py +257 -0
- gitwise/tag.py +252 -0
- gitwise/undo.py +145 -0
- gitwise/update.py +42 -0
- gitwise/utils/__init__.py +1 -0
- gitwise/utils/git_output.py +51 -0
- gitwise/utils/json_envelope.py +58 -0
- gitwise/utils/parsing.py +34 -0
- gitwise/worktree.py +182 -0
- gitwise_cli-0.24.2.dist-info/METADATA +151 -0
- gitwise_cli-0.24.2.dist-info/RECORD +125 -0
- gitwise_cli-0.24.2.dist-info/WHEEL +4 -0
- gitwise_cli-0.24.2.dist-info/entry_points.txt +2 -0
- 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
|