@oh-my-pi/pi-git-tool 3.20.0
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.
- package/CHANGELOG.md +17 -0
- package/package.json +42 -0
- package/src/cache/git-cache.ts +35 -0
- package/src/errors.ts +84 -0
- package/src/git-tool.ts +169 -0
- package/src/index.ts +3 -0
- package/src/operations/add.ts +42 -0
- package/src/operations/blame.ts +23 -0
- package/src/operations/branch.ts +115 -0
- package/src/operations/checkout.ts +37 -0
- package/src/operations/cherry-pick.ts +39 -0
- package/src/operations/commit.ts +52 -0
- package/src/operations/diff.ts +206 -0
- package/src/operations/fetch.ts +49 -0
- package/src/operations/github/ci.ts +155 -0
- package/src/operations/github/issue.ts +132 -0
- package/src/operations/github/pr.ts +247 -0
- package/src/operations/github/release.ts +109 -0
- package/src/operations/log.ts +63 -0
- package/src/operations/merge.ts +62 -0
- package/src/operations/pull.ts +47 -0
- package/src/operations/push.ts +59 -0
- package/src/operations/rebase.ts +36 -0
- package/src/operations/restore.ts +19 -0
- package/src/operations/show.ts +102 -0
- package/src/operations/stash.ts +86 -0
- package/src/operations/status.ts +54 -0
- package/src/operations/tag.ts +79 -0
- package/src/parsers/blame-parser.ts +91 -0
- package/src/parsers/diff-parser.ts +162 -0
- package/src/parsers/log-parser.ts +43 -0
- package/src/parsers/status-parser.ts +93 -0
- package/src/render.ts +181 -0
- package/src/safety/guards.ts +144 -0
- package/src/safety/policies.ts +25 -0
- package/src/types.ts +668 -0
- package/src/utils.ts +128 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PRActionResult,
|
|
3
|
+
PRCreateResult,
|
|
4
|
+
PRInfo,
|
|
5
|
+
PRListResult,
|
|
6
|
+
PRParams,
|
|
7
|
+
PRResult,
|
|
8
|
+
ToolError,
|
|
9
|
+
ToolResult,
|
|
10
|
+
} from "../../types";
|
|
11
|
+
import { gh } from "../../utils";
|
|
12
|
+
|
|
13
|
+
function parseChecks(raw: unknown): { passing: number; failing: number; pending: number } | undefined {
|
|
14
|
+
if (!raw) return undefined;
|
|
15
|
+
if (Array.isArray(raw)) {
|
|
16
|
+
let passing = 0;
|
|
17
|
+
let failing = 0;
|
|
18
|
+
let pending = 0;
|
|
19
|
+
for (const check of raw) {
|
|
20
|
+
const status =
|
|
21
|
+
(check as { state?: string; conclusion?: string }).conclusion ?? (check as { state?: string }).state ?? "";
|
|
22
|
+
switch (status) {
|
|
23
|
+
case "SUCCESS":
|
|
24
|
+
case "success":
|
|
25
|
+
passing += 1;
|
|
26
|
+
break;
|
|
27
|
+
case "FAILURE":
|
|
28
|
+
case "failure":
|
|
29
|
+
case "ERROR":
|
|
30
|
+
case "error":
|
|
31
|
+
failing += 1;
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
pending += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { passing, failing, pending };
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type GhPr = {
|
|
43
|
+
number: number;
|
|
44
|
+
title: string;
|
|
45
|
+
state: string;
|
|
46
|
+
author?: { login?: string } | string;
|
|
47
|
+
headRefName?: string;
|
|
48
|
+
baseRefName?: string;
|
|
49
|
+
branch?: string;
|
|
50
|
+
base?: string;
|
|
51
|
+
url: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
updatedAt: string;
|
|
54
|
+
additions?: number;
|
|
55
|
+
deletions?: number;
|
|
56
|
+
commits?: number;
|
|
57
|
+
reviewDecision?: string | null;
|
|
58
|
+
checks?: unknown;
|
|
59
|
+
statusCheckRollup?: unknown;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function mapPrInfo(raw: GhPr): PRInfo {
|
|
63
|
+
const author = typeof raw.author === "string" ? raw.author : (raw.author?.login ?? "");
|
|
64
|
+
return {
|
|
65
|
+
number: raw.number,
|
|
66
|
+
title: raw.title,
|
|
67
|
+
state: raw.state,
|
|
68
|
+
author,
|
|
69
|
+
branch: raw.headRefName ?? raw.branch ?? "",
|
|
70
|
+
base: raw.baseRefName ?? raw.base ?? "",
|
|
71
|
+
url: raw.url,
|
|
72
|
+
createdAt: raw.createdAt,
|
|
73
|
+
updatedAt: raw.updatedAt,
|
|
74
|
+
additions: raw.additions ?? 0,
|
|
75
|
+
deletions: raw.deletions ?? 0,
|
|
76
|
+
commits: raw.commits ?? 0,
|
|
77
|
+
reviewDecision: raw.reviewDecision ?? undefined,
|
|
78
|
+
checks: parseChecks(raw.checks ?? raw.statusCheckRollup),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function pr(params: PRParams, cwd?: string): Promise<ToolResult<PRResult> | ToolError> {
|
|
83
|
+
if (params.action === "list") {
|
|
84
|
+
const args = [
|
|
85
|
+
"pr",
|
|
86
|
+
"list",
|
|
87
|
+
"--json",
|
|
88
|
+
"number,title,state,author,headRefName,baseRefName,url,createdAt,updatedAt,additions,deletions,commits,reviewDecision,checks",
|
|
89
|
+
];
|
|
90
|
+
if (params.limit) args.push("--limit", String(params.limit));
|
|
91
|
+
if (params.state) args.push("--state", params.state);
|
|
92
|
+
if (params.author) args.push("--author", params.author);
|
|
93
|
+
const result = await gh(args, { cwd });
|
|
94
|
+
if (result.error) {
|
|
95
|
+
return { error: result.error.message, code: result.error.code };
|
|
96
|
+
}
|
|
97
|
+
const raw = JSON.parse(result.stdout) as GhPr[];
|
|
98
|
+
const prs = raw.map((item) => mapPrInfo(item));
|
|
99
|
+
const data: PRListResult = { prs };
|
|
100
|
+
return { data, _rendered: `PRs: ${prs.length}` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (params.action === "view") {
|
|
104
|
+
if (!params.number) {
|
|
105
|
+
return { error: "PR number required" };
|
|
106
|
+
}
|
|
107
|
+
const result = await gh(
|
|
108
|
+
[
|
|
109
|
+
"pr",
|
|
110
|
+
"view",
|
|
111
|
+
String(params.number),
|
|
112
|
+
"--json",
|
|
113
|
+
"number,title,state,author,headRefName,baseRefName,url,createdAt,updatedAt,additions,deletions,commits,reviewDecision,checks",
|
|
114
|
+
],
|
|
115
|
+
{ cwd },
|
|
116
|
+
);
|
|
117
|
+
if (result.error) {
|
|
118
|
+
return { error: result.error.message, code: result.error.code };
|
|
119
|
+
}
|
|
120
|
+
const raw = JSON.parse(result.stdout) as GhPr;
|
|
121
|
+
const prInfo = mapPrInfo(raw);
|
|
122
|
+
return { data: { pr: prInfo }, _rendered: `PR #${prInfo.number}: ${prInfo.title}` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (params.action === "create") {
|
|
126
|
+
const args = ["pr", "create"];
|
|
127
|
+
if (params.title) args.push("--title", params.title);
|
|
128
|
+
if (params.body) args.push("--body", params.body);
|
|
129
|
+
if (params.base) args.push("--base", params.base);
|
|
130
|
+
if (params.head) args.push("--head", params.head);
|
|
131
|
+
if (params.draft) args.push("--draft");
|
|
132
|
+
const result = await gh(args, { cwd });
|
|
133
|
+
if (result.error) {
|
|
134
|
+
return { error: result.error.message, code: result.error.code };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let number = 0;
|
|
138
|
+
let url = "";
|
|
139
|
+
const viewArgs = ["pr", "view", "--json", "number,url"];
|
|
140
|
+
if (params.head) viewArgs.push("--head", params.head);
|
|
141
|
+
const viewResult = await gh(viewArgs, { cwd });
|
|
142
|
+
if (!viewResult.error && viewResult.stdout.trim().length > 0) {
|
|
143
|
+
const raw = JSON.parse(viewResult.stdout) as { number: number; url: string };
|
|
144
|
+
number = raw.number;
|
|
145
|
+
url = raw.url;
|
|
146
|
+
} else {
|
|
147
|
+
const match = result.stdout.match(/https?:\/\/\S+/);
|
|
148
|
+
if (match) {
|
|
149
|
+
url = match[0];
|
|
150
|
+
const numMatch = url.match(/pull\/(\d+)/);
|
|
151
|
+
if (numMatch) number = Number.parseInt(numMatch[1], 10);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const data: PRCreateResult = { number, url };
|
|
156
|
+
return { data, _rendered: url ? `Created PR ${url}` : "Created PR" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (params.action === "diff") {
|
|
160
|
+
if (!params.number) {
|
|
161
|
+
return { error: "PR number required" };
|
|
162
|
+
}
|
|
163
|
+
const result = await gh(["pr", "diff", String(params.number)], { cwd });
|
|
164
|
+
if (result.error) {
|
|
165
|
+
return { error: result.error.message, code: result.error.code };
|
|
166
|
+
}
|
|
167
|
+
const data: PRActionResult = { status: "success", diff: result.stdout };
|
|
168
|
+
return { data, _rendered: "PR diff" };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (params.action === "checkout") {
|
|
172
|
+
if (!params.number) {
|
|
173
|
+
return { error: "PR number required" };
|
|
174
|
+
}
|
|
175
|
+
const result = await gh(["pr", "checkout", String(params.number)], { cwd });
|
|
176
|
+
if (result.error) {
|
|
177
|
+
return { error: result.error.message, code: result.error.code };
|
|
178
|
+
}
|
|
179
|
+
const data: PRActionResult = { status: "success" };
|
|
180
|
+
return { data, _rendered: `Checked out PR #${params.number}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (params.action === "merge") {
|
|
184
|
+
if (!params.number) {
|
|
185
|
+
return { error: "PR number required" };
|
|
186
|
+
}
|
|
187
|
+
const args = ["pr", "merge", String(params.number)];
|
|
188
|
+
if (params.merge_method === "merge") args.push("--merge");
|
|
189
|
+
if (params.merge_method === "squash") args.push("--squash");
|
|
190
|
+
if (params.merge_method === "rebase") args.push("--rebase");
|
|
191
|
+
const result = await gh(args, { cwd });
|
|
192
|
+
if (result.error) {
|
|
193
|
+
return { error: result.error.message, code: result.error.code };
|
|
194
|
+
}
|
|
195
|
+
const data: PRActionResult = { status: "success" };
|
|
196
|
+
return { data, _rendered: `Merged PR #${params.number}` };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (params.action === "close") {
|
|
200
|
+
if (!params.number) {
|
|
201
|
+
return { error: "PR number required" };
|
|
202
|
+
}
|
|
203
|
+
const result = await gh(["pr", "close", String(params.number)], { cwd });
|
|
204
|
+
if (result.error) {
|
|
205
|
+
return { error: result.error.message, code: result.error.code };
|
|
206
|
+
}
|
|
207
|
+
const data: PRActionResult = { status: "success" };
|
|
208
|
+
return { data, _rendered: `Closed PR #${params.number}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (params.action === "ready") {
|
|
212
|
+
if (!params.number) {
|
|
213
|
+
return { error: "PR number required" };
|
|
214
|
+
}
|
|
215
|
+
const result = await gh(["pr", "ready", String(params.number)], { cwd });
|
|
216
|
+
if (result.error) {
|
|
217
|
+
return { error: result.error.message, code: result.error.code };
|
|
218
|
+
}
|
|
219
|
+
const data: PRActionResult = { status: "success" };
|
|
220
|
+
return { data, _rendered: `Marked PR #${params.number} ready` };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (params.action === "review") {
|
|
224
|
+
if (!params.number) {
|
|
225
|
+
return { error: "PR number required" };
|
|
226
|
+
}
|
|
227
|
+
if (!params.review_action) {
|
|
228
|
+
return { error: "review_action required" };
|
|
229
|
+
}
|
|
230
|
+
if (params.review_action === "comment" && !params.review_body) {
|
|
231
|
+
return { error: "review_body required for comment review" };
|
|
232
|
+
}
|
|
233
|
+
const args = ["pr", "review", String(params.number)];
|
|
234
|
+
if (params.review_action === "approve") args.push("--approve");
|
|
235
|
+
if (params.review_action === "request-changes") args.push("--request-changes");
|
|
236
|
+
if (params.review_action === "comment") args.push("--comment");
|
|
237
|
+
if (params.review_body) args.push("--body", params.review_body);
|
|
238
|
+
const result = await gh(args, { cwd });
|
|
239
|
+
if (result.error) {
|
|
240
|
+
return { error: result.error.message, code: result.error.code };
|
|
241
|
+
}
|
|
242
|
+
const data: PRActionResult = { status: "success" };
|
|
243
|
+
return { data, _rendered: `Reviewed PR #${params.number}` };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { error: `Unknown PR action: ${params.action}` };
|
|
247
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReleaseInfo, ReleaseListResult, ReleaseParams, ReleaseResult, ToolError, ToolResult } from "../../types";
|
|
2
|
+
import { gh } from "../../utils";
|
|
3
|
+
|
|
4
|
+
type GhRelease = {
|
|
5
|
+
tagName?: string;
|
|
6
|
+
tag?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
body?: string;
|
|
9
|
+
isDraft?: boolean;
|
|
10
|
+
draft?: boolean;
|
|
11
|
+
isPrerelease?: boolean;
|
|
12
|
+
prerelease?: boolean;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
publishedAt?: string;
|
|
15
|
+
url: string;
|
|
16
|
+
assets?: Array<{ name: string; size: number; downloadCount?: number; download_count?: number }>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function mapReleaseInfo(raw: GhRelease): ReleaseInfo {
|
|
20
|
+
return {
|
|
21
|
+
tag: raw.tagName ?? raw.tag ?? "",
|
|
22
|
+
name: raw.name ?? "",
|
|
23
|
+
body: raw.body ?? "",
|
|
24
|
+
draft: raw.isDraft ?? raw.draft ?? false,
|
|
25
|
+
prerelease: raw.isPrerelease ?? raw.prerelease ?? false,
|
|
26
|
+
createdAt: raw.createdAt,
|
|
27
|
+
publishedAt: raw.publishedAt ?? "",
|
|
28
|
+
url: raw.url,
|
|
29
|
+
assets: (raw.assets ?? []).map((asset) => ({
|
|
30
|
+
name: asset.name,
|
|
31
|
+
size: asset.size,
|
|
32
|
+
downloadCount: asset.downloadCount ?? asset.download_count ?? 0,
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function release(params: ReleaseParams, cwd?: string): Promise<ToolResult<ReleaseResult> | ToolError> {
|
|
38
|
+
if (params.action === "list") {
|
|
39
|
+
const args = ["release", "list", "--json", "name,tagName,createdAt,publishedAt,isDraft,isPrerelease,url,assets"];
|
|
40
|
+
if (params.limit) args.push("--limit", String(params.limit));
|
|
41
|
+
const result = await gh(args, { cwd });
|
|
42
|
+
if (result.error) {
|
|
43
|
+
return { error: result.error.message, code: result.error.code };
|
|
44
|
+
}
|
|
45
|
+
const raw = JSON.parse(result.stdout) as GhRelease[];
|
|
46
|
+
const releases = raw.map((item) => mapReleaseInfo(item));
|
|
47
|
+
const data: ReleaseListResult = { releases };
|
|
48
|
+
return { data, _rendered: `Releases: ${releases.length}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (params.action === "view") {
|
|
52
|
+
if (!params.tag) return { error: "tag required" };
|
|
53
|
+
const result = await gh(
|
|
54
|
+
[
|
|
55
|
+
"release",
|
|
56
|
+
"view",
|
|
57
|
+
params.tag,
|
|
58
|
+
"--json",
|
|
59
|
+
"name,tagName,body,createdAt,publishedAt,isDraft,isPrerelease,url,assets",
|
|
60
|
+
],
|
|
61
|
+
{ cwd },
|
|
62
|
+
);
|
|
63
|
+
if (result.error) {
|
|
64
|
+
return { error: result.error.message, code: result.error.code };
|
|
65
|
+
}
|
|
66
|
+
const info = mapReleaseInfo(JSON.parse(result.stdout) as GhRelease);
|
|
67
|
+
return { data: info, _rendered: `Release ${info.tag}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (params.action === "create") {
|
|
71
|
+
if (!params.tag) return { error: "tag required" };
|
|
72
|
+
const args = ["release", "create", params.tag];
|
|
73
|
+
if (params.title) args.push("--title", params.title);
|
|
74
|
+
if (params.notes) args.push("--notes", params.notes);
|
|
75
|
+
if (params.generate_notes) args.push("--generate-notes");
|
|
76
|
+
if (params.draft) args.push("--draft");
|
|
77
|
+
if (params.prerelease) args.push("--prerelease");
|
|
78
|
+
if (params.target) args.push("--target", params.target);
|
|
79
|
+
const result = await gh(args, { cwd });
|
|
80
|
+
if (result.error) {
|
|
81
|
+
return { error: result.error.message, code: result.error.code };
|
|
82
|
+
}
|
|
83
|
+
const urlMatch = result.stdout.match(/https?:\/\/\S+/);
|
|
84
|
+
const url = urlMatch ? urlMatch[0] : undefined;
|
|
85
|
+
return { data: { status: "success", url }, _rendered: url ? `Created release ${url}` : "Created release" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (params.action === "delete") {
|
|
89
|
+
if (!params.tag) return { error: "tag required" };
|
|
90
|
+
const result = await gh(["release", "delete", params.tag, "--yes"], { cwd });
|
|
91
|
+
if (result.error) {
|
|
92
|
+
return { error: result.error.message, code: result.error.code };
|
|
93
|
+
}
|
|
94
|
+
return { data: { status: "success" }, _rendered: `Deleted release ${params.tag}` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (params.action === "upload") {
|
|
98
|
+
if (!params.tag || !params.assets || params.assets.length === 0) {
|
|
99
|
+
return { error: "tag and assets required" };
|
|
100
|
+
}
|
|
101
|
+
const result = await gh(["release", "upload", params.tag, ...params.assets], { cwd });
|
|
102
|
+
if (result.error) {
|
|
103
|
+
return { error: result.error.message, code: result.error.code };
|
|
104
|
+
}
|
|
105
|
+
return { data: { status: "success" }, _rendered: `Uploaded assets to ${params.tag}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { error: `Unknown release action: ${params.action}` };
|
|
109
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parseLog } from "../parsers/log-parser";
|
|
2
|
+
import { renderLog } from "../render";
|
|
3
|
+
import type { Commit, LogParams, LogResult, ToolError, ToolResult } from "../types";
|
|
4
|
+
import { git, parseShortstat } from "../utils";
|
|
5
|
+
|
|
6
|
+
const LOG_FORMAT = "%H%x00%h%x00%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%P%x00%s%x00%b%x1e";
|
|
7
|
+
|
|
8
|
+
async function enrichStats(commits: Commit[], cwd?: string): Promise<void> {
|
|
9
|
+
for (const commit of commits) {
|
|
10
|
+
const result = await git(["show", "-s", "--shortstat", commit.sha], { cwd });
|
|
11
|
+
if (result.error) continue;
|
|
12
|
+
const statLine = result.stdout.split("\n").find((line) => line.includes("files changed"));
|
|
13
|
+
if (!statLine) continue;
|
|
14
|
+
const stats = parseShortstat(statLine);
|
|
15
|
+
if (stats) {
|
|
16
|
+
commit.stats = {
|
|
17
|
+
files: stats.files,
|
|
18
|
+
additions: stats.additions,
|
|
19
|
+
deletions: stats.deletions,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function log(params: LogParams, cwd?: string): Promise<ToolResult<LogResult> | ToolError> {
|
|
26
|
+
const limit = params.limit ?? 10;
|
|
27
|
+
const fetchLimit = limit + 1;
|
|
28
|
+
|
|
29
|
+
const args = ["log", `--format=${LOG_FORMAT}`, "-n", String(fetchLimit)];
|
|
30
|
+
if (params.ref) args.push(params.ref);
|
|
31
|
+
if (params.author) args.push(`--author=${params.author}`);
|
|
32
|
+
if (params.since) args.push(`--since=${params.since}`);
|
|
33
|
+
if (params.until) args.push(`--until=${params.until}`);
|
|
34
|
+
if (params.grep) args.push(`--grep=${params.grep}`);
|
|
35
|
+
if (params.merges === true) args.push("--merges");
|
|
36
|
+
if (params.merges === false) args.push("--no-merges");
|
|
37
|
+
if (params.first_parent) args.push("--first-parent");
|
|
38
|
+
if (params.paths && params.paths.length > 0) {
|
|
39
|
+
args.push("--", ...params.paths);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await git(args, { cwd });
|
|
43
|
+
if (result.error) {
|
|
44
|
+
return { error: result.error.message, code: result.error.code };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let commits = parseLog(result.stdout);
|
|
48
|
+
const hasMore = commits.length > limit;
|
|
49
|
+
if (hasMore) commits = commits.slice(0, limit);
|
|
50
|
+
|
|
51
|
+
if (params.format && params.format !== "full") {
|
|
52
|
+
for (const commit of commits) {
|
|
53
|
+
commit.message = commit.subject;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (params.stat) {
|
|
58
|
+
await enrichStats(commits, cwd);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const finalResult: LogResult = { commits, hasMore };
|
|
62
|
+
return { data: finalResult, _rendered: renderLog(finalResult) };
|
|
63
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { GitErrorCode } from "../errors";
|
|
2
|
+
import { renderMerge } from "../render";
|
|
3
|
+
import type { MergeParams, MergeResult, ToolError, ToolResult } from "../types";
|
|
4
|
+
import { git } from "../utils";
|
|
5
|
+
|
|
6
|
+
async function getConflicts(cwd?: string): Promise<string[]> {
|
|
7
|
+
const result = await git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
8
|
+
if (result.error) return [];
|
|
9
|
+
return result.stdout.split("\n").filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function merge(params: MergeParams, cwd?: string): Promise<ToolResult<MergeResult> | ToolError> {
|
|
13
|
+
if (params.abort) {
|
|
14
|
+
const result = await git(["merge", "--abort"], { cwd });
|
|
15
|
+
if (result.error) {
|
|
16
|
+
return { error: result.error.message, code: result.error.code };
|
|
17
|
+
}
|
|
18
|
+
const data: MergeResult = { status: "success" };
|
|
19
|
+
return { data, _rendered: renderMerge(data) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (params.continue) {
|
|
23
|
+
const result = await git(["merge", "--continue"], { cwd });
|
|
24
|
+
if (result.error) {
|
|
25
|
+
return { error: result.error.message, code: result.error.code };
|
|
26
|
+
}
|
|
27
|
+
const shaResult = await git(["rev-parse", "HEAD"], { cwd });
|
|
28
|
+
const data: MergeResult = { status: "success", sha: shaResult.error ? undefined : shaResult.stdout.trim() };
|
|
29
|
+
return { data, _rendered: renderMerge(data) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const args = ["merge"];
|
|
33
|
+
if (params.no_ff) args.push("--no-ff");
|
|
34
|
+
if (params.ff_only) args.push("--ff-only");
|
|
35
|
+
if (params.squash) args.push("--squash");
|
|
36
|
+
if (params.message) args.push("-m", params.message);
|
|
37
|
+
if (!params.ref) {
|
|
38
|
+
return { error: "Merge ref is required" };
|
|
39
|
+
}
|
|
40
|
+
args.push(params.ref);
|
|
41
|
+
|
|
42
|
+
const result = await git(args, { cwd });
|
|
43
|
+
if (result.error) {
|
|
44
|
+
if (result.error.code === GitErrorCode.MERGE_CONFLICT) {
|
|
45
|
+
const conflicts = await getConflicts(cwd);
|
|
46
|
+
const data: MergeResult = { status: "conflict", conflicts };
|
|
47
|
+
return { data, _rendered: renderMerge(data) };
|
|
48
|
+
}
|
|
49
|
+
return { error: result.error.message, code: result.error.code };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
53
|
+
let status: MergeResult["status"] = "success";
|
|
54
|
+
if (output.includes("already up to date")) {
|
|
55
|
+
status = "up-to-date";
|
|
56
|
+
} else if (output.includes("fast-forward")) {
|
|
57
|
+
status = "fast-forward";
|
|
58
|
+
}
|
|
59
|
+
const shaResult = await git(["rev-parse", "HEAD"], { cwd });
|
|
60
|
+
const data: MergeResult = { status, sha: shaResult.error ? undefined : shaResult.stdout.trim() };
|
|
61
|
+
return { data, _rendered: renderMerge(data) };
|
|
62
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { GitErrorCode } from "../errors";
|
|
2
|
+
import { renderPull } from "../render";
|
|
3
|
+
import type { PullParams, PullResult, ToolError, ToolResult } from "../types";
|
|
4
|
+
import { git } from "../utils";
|
|
5
|
+
|
|
6
|
+
async function getConflicts(cwd?: string): Promise<string[]> {
|
|
7
|
+
const result = await git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
8
|
+
if (result.error) return [];
|
|
9
|
+
return result.stdout.split("\n").filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function countPulledCommits(cwd?: string): Promise<number> {
|
|
13
|
+
const origResult = await git(["rev-parse", "-q", "--verify", "ORIG_HEAD"], { cwd });
|
|
14
|
+
if (origResult.error) return 0;
|
|
15
|
+
const orig = origResult.stdout.trim();
|
|
16
|
+
const countResult = await git(["rev-list", "--count", `${orig}..HEAD`], { cwd });
|
|
17
|
+
if (countResult.error) return 0;
|
|
18
|
+
return Number.parseInt(countResult.stdout.trim(), 10);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function pull(params: PullParams, cwd?: string): Promise<ToolResult<PullResult> | ToolError> {
|
|
22
|
+
const args = ["pull"];
|
|
23
|
+
if (params.rebase) args.push("--rebase");
|
|
24
|
+
if (params.ff_only) args.push("--ff-only");
|
|
25
|
+
if (params.remote) args.push(params.remote);
|
|
26
|
+
if (params.branch) args.push(params.branch);
|
|
27
|
+
|
|
28
|
+
const result = await git(args, { cwd });
|
|
29
|
+
if (result.error) {
|
|
30
|
+
if (result.error.code === GitErrorCode.MERGE_CONFLICT || result.error.code === GitErrorCode.REBASE_CONFLICT) {
|
|
31
|
+
const conflicts = await getConflicts(cwd);
|
|
32
|
+
const data: PullResult = { status: "conflict", conflicts };
|
|
33
|
+
return { data, _rendered: renderPull(data) };
|
|
34
|
+
}
|
|
35
|
+
return { error: result.error.message, code: result.error.code };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
39
|
+
if (output.includes("already up to date")) {
|
|
40
|
+
const data: PullResult = { status: "up-to-date" };
|
|
41
|
+
return { data, _rendered: renderPull(data) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const commits = await countPulledCommits(cwd);
|
|
45
|
+
const data: PullResult = { status: "success", commits };
|
|
46
|
+
return { data, _rendered: renderPull(data) };
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { renderPush } from "../render";
|
|
2
|
+
import type { PushParams, PushResult, ToolError, ToolResult } from "../types";
|
|
3
|
+
import { git } from "../utils";
|
|
4
|
+
|
|
5
|
+
async function getCurrentBranch(cwd?: string): Promise<string> {
|
|
6
|
+
const result = await git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
|
|
7
|
+
if (result.error) return "";
|
|
8
|
+
return result.stdout.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function hasRemoteRef(remote: string, branch: string, cwd?: string): Promise<boolean> {
|
|
12
|
+
const result = await git(["rev-parse", "--verify", `refs/remotes/${remote}/${branch}`], { cwd });
|
|
13
|
+
return !result.error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function countCommitsToPush(remote: string, branch: string, hasRemote: boolean, cwd?: string): Promise<number> {
|
|
17
|
+
if (hasRemote) {
|
|
18
|
+
const result = await git(["rev-list", "--count", `refs/remotes/${remote}/${branch}..${branch}`], { cwd });
|
|
19
|
+
if (result.error) return 0;
|
|
20
|
+
return Number.parseInt(result.stdout.trim(), 10);
|
|
21
|
+
}
|
|
22
|
+
const result = await git(["rev-list", "--count", branch], { cwd });
|
|
23
|
+
if (result.error) return 0;
|
|
24
|
+
return Number.parseInt(result.stdout.trim(), 10);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function push(params: PushParams, cwd?: string): Promise<ToolResult<PushResult> | ToolError> {
|
|
28
|
+
const remote = params.remote ?? "origin";
|
|
29
|
+
const branch = params.branch ?? (await getCurrentBranch(cwd));
|
|
30
|
+
if (!branch) {
|
|
31
|
+
return { error: "Branch not found", code: "BRANCH_NOT_FOUND" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (params.delete) {
|
|
35
|
+
const result = await git(["push", remote, "--delete", branch], { cwd });
|
|
36
|
+
if (result.error) {
|
|
37
|
+
return { error: result.error.message, code: result.error.code };
|
|
38
|
+
}
|
|
39
|
+
const data: PushResult = { remote, branch, commits: 0, newBranch: false };
|
|
40
|
+
return { data, _rendered: renderPush(data) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const hasRemote = await hasRemoteRef(remote, branch, cwd);
|
|
44
|
+
const commits = await countCommitsToPush(remote, branch, hasRemote, cwd);
|
|
45
|
+
|
|
46
|
+
const args = ["push", remote, branch];
|
|
47
|
+
if (params.set_upstream) args.push("--set-upstream");
|
|
48
|
+
if (params.tags) args.push("--tags");
|
|
49
|
+
if (params.force_with_lease) args.push("--force-with-lease");
|
|
50
|
+
if (params.force) args.push("--force");
|
|
51
|
+
|
|
52
|
+
const result = await git(args, { cwd });
|
|
53
|
+
if (result.error) {
|
|
54
|
+
return { error: result.error.message, code: result.error.code };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data: PushResult = { remote, branch, commits, newBranch: !hasRemote };
|
|
58
|
+
return { data, _rendered: renderPush(data) };
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { GitErrorCode } from "../errors";
|
|
2
|
+
import { renderRebase } from "../render";
|
|
3
|
+
import type { RebaseParams, RebaseResult, ToolError, ToolResult } from "../types";
|
|
4
|
+
import { git } from "../utils";
|
|
5
|
+
|
|
6
|
+
async function getConflicts(cwd?: string): Promise<string[]> {
|
|
7
|
+
const result = await git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
8
|
+
if (result.error) return [];
|
|
9
|
+
return result.stdout.split("\n").filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function rebase(params: RebaseParams, cwd?: string): Promise<ToolResult<RebaseResult> | ToolError> {
|
|
13
|
+
const args = ["rebase"];
|
|
14
|
+
if (params.abort) args.push("--abort");
|
|
15
|
+
if (params.continue) args.push("--continue");
|
|
16
|
+
if (params.skip) args.push("--skip");
|
|
17
|
+
if (!params.abort && !params.continue && !params.skip) {
|
|
18
|
+
if (params.onto) args.push("--onto", params.onto);
|
|
19
|
+
if (params.upstream) args.push(params.upstream);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = await git(args, { cwd });
|
|
23
|
+
if (result.error) {
|
|
24
|
+
if (result.error.code === GitErrorCode.REBASE_CONFLICT || result.error.code === GitErrorCode.MERGE_CONFLICT) {
|
|
25
|
+
const conflicts = await getConflicts(cwd);
|
|
26
|
+
const data: RebaseResult = { status: "conflict", conflicts };
|
|
27
|
+
return { data, _rendered: renderRebase(data) };
|
|
28
|
+
}
|
|
29
|
+
return { error: result.error.message, code: result.error.code };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
33
|
+
const status: RebaseResult["status"] = output.includes("up to date") ? "up-to-date" : "success";
|
|
34
|
+
const data: RebaseResult = { status };
|
|
35
|
+
return { data, _rendered: renderRebase(data) };
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { renderRestore } from "../render";
|
|
2
|
+
import type { RestoreParams, RestoreResult, ToolError, ToolResult } from "../types";
|
|
3
|
+
import { git } from "../utils";
|
|
4
|
+
|
|
5
|
+
export async function restore(params: RestoreParams, cwd?: string): Promise<ToolResult<RestoreResult> | ToolError> {
|
|
6
|
+
const args = ["restore"];
|
|
7
|
+
if (params.staged) args.push("--staged");
|
|
8
|
+
if (params.worktree) args.push("--worktree");
|
|
9
|
+
if (params.source) args.push(`--source=${params.source}`);
|
|
10
|
+
args.push("--", ...params.paths);
|
|
11
|
+
|
|
12
|
+
const result = await git(args, { cwd });
|
|
13
|
+
if (result.error) {
|
|
14
|
+
return { error: result.error.message, code: result.error.code };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const data: RestoreResult = { restored: params.paths };
|
|
18
|
+
return { data, _rendered: renderRestore(data) };
|
|
19
|
+
}
|