-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-reset-soft-tool.ts
More file actions
96 lines (86 loc) · 3.36 KB
/
git-reset-soft-tool.ts
File metadata and controls
96 lines (86 loc) · 3.36 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
import type { FastMCP } from "fastmcp";
import { z } from "zod";
import { spawnGitAsync } from "./git.js";
import { isSafeGitAncestorRef, isWorkingTreeClean } from "./git-refs.js";
import { jsonRespond, spreadDefined } from "./json.js";
import { requireSingleRepo } from "./roots.js";
import { WorkspacePickSchema } from "./schemas.js";
export function registerGitResetSoftTool(server: FastMCP): void {
server.addTool({
name: "git_reset_soft",
description:
"Soft-reset the current branch to a reference (`git reset --soft <ref>`). " +
"Moves the branch pointer back while keeping all changes from the rewound commits " +
"in the staging index — use this to re-split an already-committed chunk. " +
"Refuses when the working tree has any uncommitted or unstaged changes (run on a clean tree).",
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
parameters: WorkspacePickSchema.omit({ absoluteGitRoots: true }).extend({
ref: z
.string()
.min(1)
.describe(
"Commit to reset to. Accepts ancestor notation (`HEAD~1`, `HEAD~3`), " +
"branch names, or full SHAs.",
),
}),
execute: async (args) => {
const pre = requireSingleRepo(server, args);
if (!pre.ok) return jsonRespond(pre.error);
const { gitTop } = pre;
// Validate ref — allow ancestor notation (~N, ^N).
if (!isSafeGitAncestorRef(args.ref)) {
return jsonRespond({ error: "unsafe_ref_token", ref: args.ref });
}
// Refuse when the working tree is dirty (unstaged or untracked changes).
if (!(await isWorkingTreeClean(gitTop))) {
return jsonRespond({
error: "working_tree_dirty",
detail:
"git_reset_soft requires a clean working tree. " +
"Commit or stash pending changes first.",
});
}
// Probe HEAD before reset for the response.
const preSha = await spawnGitAsync(gitTop, ["rev-parse", "HEAD"]);
const beforeSha = preSha.ok ? preSha.stdout.trim() : undefined;
// Run the reset.
const r = await spawnGitAsync(gitTop, ["reset", "--soft", args.ref]);
if (!r.ok) {
return jsonRespond({
error: "reset_failed",
detail: (r.stderr || r.stdout).trim(),
});
}
// Probe HEAD after reset.
const postSha = await spawnGitAsync(gitTop, ["rev-parse", "HEAD"]);
const afterSha = postSha.ok ? postSha.stdout.trim() : undefined;
// Count staged changes after reset.
const stagedResult = await spawnGitAsync(gitTop, ["diff", "--cached", "--name-only"]);
const stagedFiles = stagedResult.ok
? stagedResult.stdout
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0)
: [];
if (args.format === "json") {
return jsonRespond({
ok: true,
ref: args.ref,
...spreadDefined("beforeSha", beforeSha),
...spreadDefined("afterSha", afterSha),
stagedCount: stagedFiles.length,
});
}
const beforeShort = beforeSha?.slice(0, 7) ?? "?";
const afterShort = afterSha?.slice(0, 7) ?? "?";
return [
"# Reset (soft)",
`✓ ${beforeShort} → ${afterShort} (${stagedFiles.length} file(s) staged)`,
].join("\n");
},
});
}