-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-stash-tool.ts
More file actions
149 lines (133 loc) · 4.82 KB
/
git-stash-tool.ts
File metadata and controls
149 lines (133 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import type { FastMCP } from "fastmcp";
import { z } from "zod";
import { ERROR_CODES } from "./error-codes.js";
import { spawnGitAsync } from "./git.js";
import { jsonRespond, spreadDefined } from "./json.js";
import { requireSingleRepo } from "./roots.js";
import { WorkspacePickSchema } from "./schemas.js";
// ---------------------------------------------------------------------------
// git_stash_list
// ---------------------------------------------------------------------------
export function registerGitStashListTool(server: FastMCP): void {
server.addTool({
name: "git_stash_list",
description: "List all git stashes.",
annotations: {
readOnlyHint: true,
},
parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true, allWorkspaceRoots: true }).pick({
workspaceRoot: true,
rootIndex: true,
format: true,
}),
execute: async (args) => {
const pre = requireSingleRepo(server, args);
if (!pre.ok) return jsonRespond(pre.error);
const { gitTop } = pre;
// List all stashes: git stash list --format='%(refname:short)|%(subject)|%(objectname:short)'
// git stash list uses git-log format (%s, %h) not for-each-ref format %(subject).
const r = await spawnGitAsync(gitTop, ["stash", "list", "--format=%gd|%s|%h"]);
if (!r.ok) {
// If there are no stashes, git still returns ok=true with empty output
// Only treat as error if git itself failed
return jsonRespond({
error: ERROR_CODES.STASH_LIST_FAILED,
detail: (r.stderr || r.stdout).trim(),
});
}
const stashes: Array<{ index: number; message: string; sha: string }> = [];
const lines = (r.stdout || "")
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0);
for (const line of lines) {
const parts = line.split("|");
// parts[0] = stash@{N}, last part = short SHA, middle = message (may contain "|")
const sha = parts[parts.length - 1];
const message = parts.slice(1, -1).join("|");
// Parse the real stash index from the canonical stash@{N} ref in parts[0].
const indexMatch = parts[0] ? /stash@\{(\d+)\}/.exec(parts[0]) : null;
if (!indexMatch || parts.length < 3 || !message || !sha) {
// Malformed line — skip without affecting index tracking.
continue;
}
stashes.push({
index: Number(indexMatch[1]),
message,
sha,
});
}
if (args.format === "json") {
return jsonRespond({ stashes });
}
if (stashes.length === 0) {
return "# Stashes\n_(none)_";
}
const lines_out: string[] = ["# Stashes", ""];
for (const s of stashes) {
lines_out.push(`- **stash@{${s.index}}** — ${s.message} (\`${s.sha}\`)`);
}
return lines_out.join("\n");
},
});
}
// ---------------------------------------------------------------------------
// git_stash_apply
// ---------------------------------------------------------------------------
export function registerGitStashApplyTool(server: FastMCP): void {
server.addTool({
name: "git_stash_apply",
description:
"Apply or pop a git stash. `index` defaults to 0. `pop: true` removes stash after applying.",
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true, allWorkspaceRoots: true })
.pick({
workspaceRoot: true,
rootIndex: true,
format: true,
})
.extend({
index: z
.number()
.int()
.min(0)
.optional()
.default(0)
.describe("Stash index (defaults to 0 for stash@{0})."),
pop: z
.boolean()
.optional()
.default(false)
.describe(
"Run `git stash pop` instead of `git stash apply` (removes stash after applying).",
),
}),
execute: async (args) => {
const pre = requireSingleRepo(server, args);
if (!pre.ok) return jsonRespond(pre.error);
const { gitTop } = pre;
const stashRef = `stash@{${args.index}}`;
const cmd = args.pop ? "pop" : "apply";
const r = await spawnGitAsync(gitTop, ["stash", cmd, stashRef]);
const applied = r.ok;
const output = (r.stdout || r.stderr).trim();
if (args.format === "json") {
return jsonRespond({
applied,
stashIndex: args.index,
popped: args.pop,
...spreadDefined("output", output),
});
}
const verb = args.pop ? "popped" : "applied";
if (applied) {
return `# Stash ${verb}\n✓ ${stashRef} → ${verb}`;
}
return `# Stash ${verb} (failed)\n✗ ${stashRef}\n\n\`\`\`\n${output}\n\`\`\``;
},
});
}