@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,102 @@
|
|
|
1
|
+
import { parseDiff } from "../parsers/diff-parser";
|
|
2
|
+
import { parseLog } from "../parsers/log-parser";
|
|
3
|
+
import { renderShowCommit, renderShowFile } from "../render";
|
|
4
|
+
import type { ShowCommitResult, ShowFileResult, ShowParams, ToolError, ToolResult } from "../types";
|
|
5
|
+
import { git } from "../utils";
|
|
6
|
+
|
|
7
|
+
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";
|
|
8
|
+
|
|
9
|
+
const MAX_SHOW_LINES = 2000;
|
|
10
|
+
const MAX_SHOW_BYTES = 200_000;
|
|
11
|
+
|
|
12
|
+
export async function show(
|
|
13
|
+
params: ShowParams,
|
|
14
|
+
cwd?: string,
|
|
15
|
+
): Promise<ToolResult<ShowCommitResult | ShowFileResult> | ToolError> {
|
|
16
|
+
if (params.path) {
|
|
17
|
+
const result = await git(["show", `${params.ref}:${params.path}`], { cwd });
|
|
18
|
+
if (result.error) {
|
|
19
|
+
return { error: result.error.message, code: result.error.code };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let content = result.stdout;
|
|
23
|
+
let truncated = false;
|
|
24
|
+
|
|
25
|
+
// Ambiguity: no truncation limits specified for show file; defaulting to 2000 lines or 200KB.
|
|
26
|
+
if (content.length > MAX_SHOW_BYTES) {
|
|
27
|
+
content = content.slice(0, MAX_SHOW_BYTES);
|
|
28
|
+
truncated = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let lines = content.split("\n");
|
|
32
|
+
if (lines.length > MAX_SHOW_LINES) {
|
|
33
|
+
lines = lines.slice(0, MAX_SHOW_LINES);
|
|
34
|
+
content = lines.join("\n");
|
|
35
|
+
truncated = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (params.lines) {
|
|
39
|
+
const start = Math.max(1, params.lines.start);
|
|
40
|
+
const end = Math.max(start, params.lines.end);
|
|
41
|
+
const slice = lines.slice(start - 1, end);
|
|
42
|
+
content = slice.join("\n");
|
|
43
|
+
truncated = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data: ShowFileResult = {
|
|
47
|
+
path: params.path,
|
|
48
|
+
ref: params.ref,
|
|
49
|
+
content,
|
|
50
|
+
truncated,
|
|
51
|
+
};
|
|
52
|
+
return { data, _rendered: renderShowFile(data) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const commitResult = await git(["show", "-s", `--format=${LOG_FORMAT}`, params.ref], { cwd });
|
|
56
|
+
if (commitResult.error) {
|
|
57
|
+
return { error: commitResult.error.message, code: commitResult.error.code };
|
|
58
|
+
}
|
|
59
|
+
const commits = parseLog(commitResult.stdout);
|
|
60
|
+
const commit = commits[0];
|
|
61
|
+
if (!commit) {
|
|
62
|
+
return { error: "Commit not found", code: "REF_NOT_FOUND" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let diffData: ShowCommitResult["diff"] | undefined;
|
|
66
|
+
if (params.diff || params.stat) {
|
|
67
|
+
const diffResult = await git(["show", params.ref, "--format="], { cwd });
|
|
68
|
+
if (diffResult.error) {
|
|
69
|
+
return { error: diffResult.error.message, code: diffResult.error.code };
|
|
70
|
+
}
|
|
71
|
+
const parsed = parseDiff(diffResult.stdout);
|
|
72
|
+
const files = parsed.files.map((file) => {
|
|
73
|
+
if (!params.diff) {
|
|
74
|
+
delete file.hunks;
|
|
75
|
+
}
|
|
76
|
+
return file;
|
|
77
|
+
});
|
|
78
|
+
const stats = files.reduce(
|
|
79
|
+
(acc, file) => {
|
|
80
|
+
acc.filesChanged += 1;
|
|
81
|
+
acc.insertions += file.additions;
|
|
82
|
+
acc.deletions += file.deletions;
|
|
83
|
+
return acc;
|
|
84
|
+
},
|
|
85
|
+
{ filesChanged: 0, insertions: 0, deletions: 0 },
|
|
86
|
+
);
|
|
87
|
+
const diff: ShowCommitResult["diff"] = {
|
|
88
|
+
files,
|
|
89
|
+
stats,
|
|
90
|
+
truncated: parsed.truncated,
|
|
91
|
+
truncatedFiles: parsed.truncatedFiles.length > 0 ? parsed.truncatedFiles : undefined,
|
|
92
|
+
};
|
|
93
|
+
diffData = diff;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data: ShowCommitResult = {
|
|
97
|
+
commit,
|
|
98
|
+
...(diffData ? { diff: diffData } : {}),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return { data, _rendered: renderShowCommit(data) };
|
|
102
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { StashEntry, StashParams, StashResult, ToolError, ToolResult } from "../types";
|
|
2
|
+
import { git, parseShortstat } from "../utils";
|
|
3
|
+
|
|
4
|
+
function parseStashIndex(ref: string): number {
|
|
5
|
+
const match = ref.match(/stash@\{(\d+)\}/);
|
|
6
|
+
return match ? Number.parseInt(match[1], 10) : -1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseBranch(subject: string): string {
|
|
10
|
+
const match = subject.match(/on ([^:]+):/i);
|
|
11
|
+
return match ? match[1] : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function stash(params: StashParams, cwd?: string): Promise<ToolResult<StashResult> | ToolError> {
|
|
15
|
+
const action = params.action ?? "list";
|
|
16
|
+
|
|
17
|
+
if (action === "list") {
|
|
18
|
+
const result = await git(["stash", "list", "--date=iso-strict", "--format=%gd%x00%gs%x00%cd"], { cwd });
|
|
19
|
+
if (result.error) {
|
|
20
|
+
return { error: result.error.message, code: result.error.code };
|
|
21
|
+
}
|
|
22
|
+
const stashes: StashEntry[] = [];
|
|
23
|
+
for (const line of result.stdout.split("\n")) {
|
|
24
|
+
if (!line) continue;
|
|
25
|
+
const parts = line.split("\x00");
|
|
26
|
+
if (parts.length < 3) continue;
|
|
27
|
+
const ref = parts[0];
|
|
28
|
+
const message = parts[1];
|
|
29
|
+
const date = parts[2];
|
|
30
|
+
stashes.push({
|
|
31
|
+
index: parseStashIndex(ref),
|
|
32
|
+
message,
|
|
33
|
+
branch: parseBranch(message),
|
|
34
|
+
date,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { data: { stashes }, _rendered: `Stashes: ${stashes.length}` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (action === "show") {
|
|
41
|
+
const index = params.index ?? 0;
|
|
42
|
+
const ref = `stash@{${index}}`;
|
|
43
|
+
const statResult = await git(["stash", "show", "--shortstat", ref], { cwd });
|
|
44
|
+
if (statResult.error) {
|
|
45
|
+
return { error: statResult.error.message, code: statResult.error.code };
|
|
46
|
+
}
|
|
47
|
+
const statLine = statResult.stdout.split("\n").find((line) => line.includes("files changed"));
|
|
48
|
+
const statsParsed = statLine ? parseShortstat(statLine) : null;
|
|
49
|
+
const stats = {
|
|
50
|
+
files: statsParsed?.files ?? 0,
|
|
51
|
+
additions: statsParsed?.additions ?? 0,
|
|
52
|
+
deletions: statsParsed?.deletions ?? 0,
|
|
53
|
+
};
|
|
54
|
+
const filesResult = await git(["stash", "show", "--name-only", ref], { cwd });
|
|
55
|
+
if (filesResult.error) {
|
|
56
|
+
return { error: filesResult.error.message, code: filesResult.error.code };
|
|
57
|
+
}
|
|
58
|
+
const files = filesResult.stdout.split("\n").filter(Boolean);
|
|
59
|
+
return { data: { stats, files }, _rendered: `Stash ${index} (${files.length} files)` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action === "push") {
|
|
63
|
+
const args = ["stash", "push"];
|
|
64
|
+
if (params.message) args.push("-m", params.message);
|
|
65
|
+
if (params.include_untracked) args.push("-u");
|
|
66
|
+
if (params.keep_index) args.push("--keep-index");
|
|
67
|
+
const result = await git(args, { cwd });
|
|
68
|
+
if (result.error) {
|
|
69
|
+
return { error: result.error.message, code: result.error.code };
|
|
70
|
+
}
|
|
71
|
+
return { data: { status: "success" }, _rendered: "Stash saved" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (action === "pop" || action === "apply" || action === "drop") {
|
|
75
|
+
const index = params.index ?? 0;
|
|
76
|
+
const ref = `stash@{${index}}`;
|
|
77
|
+
const args = ["stash", action, ref];
|
|
78
|
+
const result = await git(args, { cwd });
|
|
79
|
+
if (result.error) {
|
|
80
|
+
return { error: result.error.message, code: result.error.code };
|
|
81
|
+
}
|
|
82
|
+
return { data: { status: "success" }, _rendered: `Stash ${action} ${index}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { error: `Unknown stash action: ${action}` };
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseStatus } from "../parsers/status-parser";
|
|
2
|
+
import { renderStatus } from "../render";
|
|
3
|
+
import type { StatusParams, StatusResult, ToolError, ToolResult } from "../types";
|
|
4
|
+
import { git } from "../utils";
|
|
5
|
+
|
|
6
|
+
export async function status(params: StatusParams, cwd?: string): Promise<ToolResult<StatusResult> | ToolError> {
|
|
7
|
+
const args = ["status", "--porcelain=v2", "--branch", "--ahead-behind"];
|
|
8
|
+
if (params.ignored) args.push("--ignored");
|
|
9
|
+
|
|
10
|
+
const result = await git(args, { cwd });
|
|
11
|
+
if (result.error) {
|
|
12
|
+
return { error: result.error.message, code: result.error.code };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsed = parseStatus(result.stdout, Boolean(params.ignored));
|
|
16
|
+
let finalResult = parsed;
|
|
17
|
+
|
|
18
|
+
if (params.only) {
|
|
19
|
+
const base: StatusResult = {
|
|
20
|
+
branch: parsed.branch,
|
|
21
|
+
upstream: parsed.upstream,
|
|
22
|
+
ahead: parsed.ahead,
|
|
23
|
+
behind: parsed.behind,
|
|
24
|
+
staged: [],
|
|
25
|
+
modified: [],
|
|
26
|
+
untracked: [],
|
|
27
|
+
conflicts: [],
|
|
28
|
+
};
|
|
29
|
+
switch (params.only) {
|
|
30
|
+
case "branch":
|
|
31
|
+
finalResult = base;
|
|
32
|
+
break;
|
|
33
|
+
case "modified":
|
|
34
|
+
finalResult = { ...base, modified: parsed.modified };
|
|
35
|
+
break;
|
|
36
|
+
case "staged":
|
|
37
|
+
finalResult = { ...base, staged: parsed.staged };
|
|
38
|
+
break;
|
|
39
|
+
case "untracked":
|
|
40
|
+
finalResult = { ...base, untracked: parsed.untracked };
|
|
41
|
+
break;
|
|
42
|
+
case "conflicts":
|
|
43
|
+
finalResult = { ...base, conflicts: parsed.conflicts };
|
|
44
|
+
break;
|
|
45
|
+
case "sync":
|
|
46
|
+
finalResult = base;
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
finalResult = parsed;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { data: finalResult, _rendered: renderStatus(finalResult) };
|
|
54
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { renderTag } from "../render";
|
|
2
|
+
import type { TagInfo, TagParams, TagResult, ToolError, ToolResult } from "../types";
|
|
3
|
+
import { git } from "../utils";
|
|
4
|
+
|
|
5
|
+
export async function tag(params: TagParams, cwd?: string): Promise<ToolResult<TagResult> | ToolError> {
|
|
6
|
+
const action = params.action ?? "list";
|
|
7
|
+
|
|
8
|
+
if (action === "list") {
|
|
9
|
+
const result = await git(
|
|
10
|
+
[
|
|
11
|
+
"for-each-ref",
|
|
12
|
+
"refs/tags",
|
|
13
|
+
"--format=%(refname:short)%x00%(objectname)%x00%(taggername)%x00%(taggerdate:iso-strict)%x00%(subject)%x00%(objecttype)",
|
|
14
|
+
],
|
|
15
|
+
{ cwd },
|
|
16
|
+
);
|
|
17
|
+
if (result.error) {
|
|
18
|
+
return { error: result.error.message, code: result.error.code };
|
|
19
|
+
}
|
|
20
|
+
const tags: TagInfo[] = [];
|
|
21
|
+
for (const line of result.stdout.split("\n")) {
|
|
22
|
+
if (!line) continue;
|
|
23
|
+
const parts = line.split("\x00");
|
|
24
|
+
if (parts.length < 6) continue;
|
|
25
|
+
const [name, sha, taggerName, taggerDate, subject, objectType] = parts;
|
|
26
|
+
const annotated = objectType === "tag";
|
|
27
|
+
const tagger = taggerName && taggerDate ? { name: taggerName, date: taggerDate } : undefined;
|
|
28
|
+
tags.push({ name, sha, message: subject || undefined, tagger, annotated });
|
|
29
|
+
}
|
|
30
|
+
const data: TagResult = { tags };
|
|
31
|
+
return { data, _rendered: renderTag(data) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (action === "create") {
|
|
35
|
+
if (!params.name) {
|
|
36
|
+
return { error: "Tag name required" };
|
|
37
|
+
}
|
|
38
|
+
const args = ["tag"];
|
|
39
|
+
if (params.force) args.push("-f");
|
|
40
|
+
if (params.sign) args.push("-s");
|
|
41
|
+
if (params.message) {
|
|
42
|
+
args.push("-a", "-m", params.message);
|
|
43
|
+
}
|
|
44
|
+
args.push(params.name);
|
|
45
|
+
if (params.ref) args.push(params.ref);
|
|
46
|
+
const result = await git(args, { cwd });
|
|
47
|
+
if (result.error) {
|
|
48
|
+
return { error: result.error.message, code: result.error.code };
|
|
49
|
+
}
|
|
50
|
+
const data: TagResult = { status: "success" };
|
|
51
|
+
return { data, _rendered: renderTag(data) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (action === "delete") {
|
|
55
|
+
if (!params.name) {
|
|
56
|
+
return { error: "Tag name required" };
|
|
57
|
+
}
|
|
58
|
+
const result = await git(["tag", "-d", params.name], { cwd });
|
|
59
|
+
if (result.error) {
|
|
60
|
+
return { error: result.error.message, code: result.error.code };
|
|
61
|
+
}
|
|
62
|
+
const data: TagResult = { status: "success" };
|
|
63
|
+
return { data, _rendered: renderTag(data) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (action === "push") {
|
|
67
|
+
if (!params.name) {
|
|
68
|
+
return { error: "Tag name required" };
|
|
69
|
+
}
|
|
70
|
+
const result = await git(["push", "origin", params.name], { cwd });
|
|
71
|
+
if (result.error) {
|
|
72
|
+
return { error: result.error.message, code: result.error.code };
|
|
73
|
+
}
|
|
74
|
+
const data: TagResult = { status: "success" };
|
|
75
|
+
return { data, _rendered: renderTag(data) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { error: `Unknown tag action: ${action}` };
|
|
79
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { BlameLine } from "../types";
|
|
2
|
+
|
|
3
|
+
interface BlameMeta {
|
|
4
|
+
sha: string;
|
|
5
|
+
shortSha: string;
|
|
6
|
+
author: string;
|
|
7
|
+
date: string;
|
|
8
|
+
lineNo: number;
|
|
9
|
+
original?: { sha: string; path: string; lineNo: number };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatDate(timestamp: string): string {
|
|
13
|
+
const seconds = Number.parseInt(timestamp, 10);
|
|
14
|
+
if (!Number.isFinite(seconds)) return "";
|
|
15
|
+
return new Date(seconds * 1000).toISOString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseBlame(output: string): BlameLine[] {
|
|
19
|
+
const lines = output.split("\n");
|
|
20
|
+
const result: BlameLine[] = [];
|
|
21
|
+
let current: BlameMeta | null = null;
|
|
22
|
+
let remaining = 0;
|
|
23
|
+
let currentFilename = "";
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (!line) continue;
|
|
27
|
+
if (line.startsWith("\t")) {
|
|
28
|
+
if (!current) continue;
|
|
29
|
+
result.push({
|
|
30
|
+
lineNo: current.lineNo,
|
|
31
|
+
sha: current.sha,
|
|
32
|
+
shortSha: current.shortSha,
|
|
33
|
+
author: current.author,
|
|
34
|
+
date: current.date,
|
|
35
|
+
content: line.slice(1),
|
|
36
|
+
original: current.original,
|
|
37
|
+
});
|
|
38
|
+
current.lineNo += 1;
|
|
39
|
+
remaining -= 1;
|
|
40
|
+
if (remaining <= 0) {
|
|
41
|
+
current = null;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const headerMatch = line.match(/^([0-9a-f]{40}) (\d+) (\d+) (\d+)/);
|
|
47
|
+
if (headerMatch) {
|
|
48
|
+
const sha = headerMatch[1];
|
|
49
|
+
const finalLine = Number.parseInt(headerMatch[3], 10);
|
|
50
|
+
remaining = Number.parseInt(headerMatch[4], 10);
|
|
51
|
+
current = {
|
|
52
|
+
sha,
|
|
53
|
+
shortSha: sha.slice(0, 7),
|
|
54
|
+
author: "",
|
|
55
|
+
date: "",
|
|
56
|
+
lineNo: finalLine,
|
|
57
|
+
};
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!current) continue;
|
|
62
|
+
|
|
63
|
+
if (line.startsWith("author ")) {
|
|
64
|
+
current.author = line.slice(7).trim();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.startsWith("author-time ")) {
|
|
68
|
+
current.date = formatDate(line.slice(12).trim());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (line.startsWith("previous ")) {
|
|
72
|
+
const parts = line.split(" ");
|
|
73
|
+
if (parts.length >= 3) {
|
|
74
|
+
current.original = {
|
|
75
|
+
sha: parts[1],
|
|
76
|
+
path: currentFilename,
|
|
77
|
+
lineNo: current.lineNo,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (line.startsWith("filename ")) {
|
|
83
|
+
currentFilename = line.slice(9).trim();
|
|
84
|
+
if (current.original) {
|
|
85
|
+
current.original.path = currentFilename;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { DiffLine, FileDiff, Hunk } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface DiffParseResult {
|
|
4
|
+
files: FileDiff[];
|
|
5
|
+
truncated: boolean;
|
|
6
|
+
truncatedFiles: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DiffParseOptions {
|
|
10
|
+
maxLines?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseHunkHeader(header: string): { oldStart: number; oldCount: number; newStart: number; newCount: number } {
|
|
14
|
+
const match = header.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
15
|
+
if (!match) {
|
|
16
|
+
return { oldStart: 0, oldCount: 0, newStart: 0, newCount: 0 };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
oldStart: Number.parseInt(match[1], 10),
|
|
20
|
+
oldCount: Number.parseInt(match[2] ?? "1", 10),
|
|
21
|
+
newStart: Number.parseInt(match[3], 10),
|
|
22
|
+
newCount: Number.parseInt(match[4] ?? "1", 10),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseDiff(raw: string, options?: DiffParseOptions): DiffParseResult {
|
|
27
|
+
const maxLines = options?.maxLines ?? Number.POSITIVE_INFINITY;
|
|
28
|
+
const lines = raw.split("\n");
|
|
29
|
+
const files: FileDiff[] = [];
|
|
30
|
+
const truncatedFiles: string[] = [];
|
|
31
|
+
let truncated = false;
|
|
32
|
+
let lineCount = 0;
|
|
33
|
+
|
|
34
|
+
let current: FileDiff | null = null;
|
|
35
|
+
let currentHunk: Hunk | null = null;
|
|
36
|
+
let oldLine = 0;
|
|
37
|
+
let newLine = 0;
|
|
38
|
+
|
|
39
|
+
const finalizeHunk = () => {
|
|
40
|
+
if (current && currentHunk) {
|
|
41
|
+
current.hunks = current.hunks ?? [];
|
|
42
|
+
current.hunks.push(currentHunk);
|
|
43
|
+
currentHunk = null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const finalizeFile = () => {
|
|
48
|
+
finalizeHunk();
|
|
49
|
+
if (current) {
|
|
50
|
+
files.push(current);
|
|
51
|
+
current = null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
if (line.startsWith("diff --git ")) {
|
|
57
|
+
finalizeFile();
|
|
58
|
+
const match = line.match(/^diff --git a\/(.*) b\/(.*)$/);
|
|
59
|
+
const path = match ? match[2] : line.slice("diff --git ".length).trim();
|
|
60
|
+
const oldPath = match ? match[1] : undefined;
|
|
61
|
+
current = {
|
|
62
|
+
path,
|
|
63
|
+
oldPath,
|
|
64
|
+
status: "modified",
|
|
65
|
+
binary: false,
|
|
66
|
+
additions: 0,
|
|
67
|
+
deletions: 0,
|
|
68
|
+
};
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!current) continue;
|
|
73
|
+
|
|
74
|
+
if (line.startsWith("new file mode")) {
|
|
75
|
+
current.status = "added";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (line.startsWith("deleted file mode")) {
|
|
79
|
+
current.status = "deleted";
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (line.startsWith("rename from ")) {
|
|
83
|
+
current.status = "renamed";
|
|
84
|
+
current.oldPath = line.slice("rename from ".length).trim();
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (line.startsWith("rename to ")) {
|
|
88
|
+
current.path = line.slice("rename to ".length).trim();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (line.startsWith("copy from ")) {
|
|
92
|
+
current.status = "copied";
|
|
93
|
+
current.oldPath = line.slice("copy from ".length).trim();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (line.startsWith("copy to ")) {
|
|
97
|
+
current.path = line.slice("copy to ".length).trim();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
|
|
101
|
+
current.binary = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (line.startsWith("@@ ")) {
|
|
106
|
+
finalizeHunk();
|
|
107
|
+
const header = line;
|
|
108
|
+
const ranges = parseHunkHeader(header);
|
|
109
|
+
currentHunk = {
|
|
110
|
+
oldStart: ranges.oldStart,
|
|
111
|
+
oldCount: ranges.oldCount,
|
|
112
|
+
newStart: ranges.newStart,
|
|
113
|
+
newCount: ranges.newCount,
|
|
114
|
+
header,
|
|
115
|
+
lines: [],
|
|
116
|
+
};
|
|
117
|
+
oldLine = ranges.oldStart;
|
|
118
|
+
newLine = ranges.newStart;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!currentHunk) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (lineCount >= maxLines) {
|
|
127
|
+
if (!truncated) {
|
|
128
|
+
truncated = true;
|
|
129
|
+
truncatedFiles.push(current.path);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (line.startsWith("\\")) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let diffLine: DiffLine | null = null;
|
|
139
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
140
|
+
diffLine = { type: "add", content: line.slice(1), newLineNo: newLine };
|
|
141
|
+
newLine += 1;
|
|
142
|
+
current.additions += 1;
|
|
143
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
144
|
+
diffLine = { type: "delete", content: line.slice(1), oldLineNo: oldLine };
|
|
145
|
+
oldLine += 1;
|
|
146
|
+
current.deletions += 1;
|
|
147
|
+
} else if (line.startsWith(" ")) {
|
|
148
|
+
diffLine = { type: "context", content: line.slice(1), oldLineNo: oldLine, newLineNo: newLine };
|
|
149
|
+
oldLine += 1;
|
|
150
|
+
newLine += 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (diffLine) {
|
|
154
|
+
currentHunk.lines.push(diffLine);
|
|
155
|
+
lineCount += 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
finalizeFile();
|
|
160
|
+
|
|
161
|
+
return { files, truncated, truncatedFiles };
|
|
162
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Commit } from "../types";
|
|
2
|
+
|
|
3
|
+
const RECORD_SEPARATOR = "\x1e";
|
|
4
|
+
const FIELD_SEPARATOR = "\x00";
|
|
5
|
+
|
|
6
|
+
export function parseLog(output: string): Commit[] {
|
|
7
|
+
const records = output.split(RECORD_SEPARATOR).filter((record) => record.trim().length > 0);
|
|
8
|
+
const commits: Commit[] = [];
|
|
9
|
+
|
|
10
|
+
for (const record of records) {
|
|
11
|
+
const fields = record.split(FIELD_SEPARATOR);
|
|
12
|
+
if (fields.length < 11) continue;
|
|
13
|
+
|
|
14
|
+
const [
|
|
15
|
+
sha,
|
|
16
|
+
shortSha,
|
|
17
|
+
authorName,
|
|
18
|
+
authorEmail,
|
|
19
|
+
authorDate,
|
|
20
|
+
committerName,
|
|
21
|
+
committerEmail,
|
|
22
|
+
committerDate,
|
|
23
|
+
parentsRaw,
|
|
24
|
+
subject,
|
|
25
|
+
body,
|
|
26
|
+
] = fields;
|
|
27
|
+
|
|
28
|
+
const message = body ? `${subject}\n\n${body}` : subject;
|
|
29
|
+
const parents = parentsRaw ? parentsRaw.split(" ").filter(Boolean) : [];
|
|
30
|
+
|
|
31
|
+
commits.push({
|
|
32
|
+
sha,
|
|
33
|
+
shortSha,
|
|
34
|
+
author: { name: authorName, email: authorEmail, date: authorDate },
|
|
35
|
+
committer: { name: committerName, email: committerEmail, date: committerDate },
|
|
36
|
+
message,
|
|
37
|
+
subject,
|
|
38
|
+
parents,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return commits;
|
|
43
|
+
}
|