Repository navigation
Expand file tree
/
Copy pathmemory-git.ts
More file actions
166 lines (131 loc) · 5.94 KB
/
Copy pathmemory-git.ts
File metadata and controls
166 lines (131 loc) · 5.94 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import fs from "node:fs";
import path from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import type { GitResult, MemoryMdSettings, SyncResult } from "./types.js";
import { DEFAULT_LOCAL_PATH, formatCommitTimestamp, getProjectMeta } from "./utils.js";
const TIMEOUT_MS = 10000;
const TIMEOUT_MESSAGE =
"Unable to connect to git repository, connection timeout (10s). Please check your network connection or try again later.";
function getRepoName(settings: MemoryMdSettings): string {
if (!settings.repoUrl) return "memory-md";
const match = settings.repoUrl.match(/\/([^/]+?)(\.git)?$/);
return match ? match[1] : "memory-md";
}
async function hasUpstreamBranch(pi: ExtensionAPI, cwd: string): Promise<boolean> {
const upstreamResult = await gitExec(pi, cwd, ["rev-parse", "--abbrev-ref", "@{u}"]);
return upstreamResult.success;
}
async function isAlreadyAtUpstream(pi: ExtensionAPI, cwd: string): Promise<boolean> {
if (!(await hasUpstreamBranch(pi, cwd))) return false;
const [headResult, trackedResult] = await Promise.all([
gitExec(pi, cwd, ["rev-parse", "HEAD"]),
gitExec(pi, cwd, ["rev-parse", "@{u}"]),
]);
return headResult.success && trackedResult.success && headResult.stdout.trim() === trackedResult.stdout.trim();
}
async function hasCommitsToPush(pi: ExtensionAPI, cwd: string): Promise<boolean> {
if (!(await hasUpstreamBranch(pi, cwd))) return true;
const aheadResult = await gitExec(pi, cwd, ["rev-list", "--count", "@{u}..HEAD"]);
if (!aheadResult.success) return true;
return Number(aheadResult.stdout.trim() || "0") > 0;
}
export async function gitExec(
pi: ExtensionAPI,
cwd: string,
args: string[],
timeoutMs = TIMEOUT_MS,
): Promise<GitResult> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await pi.exec("git", args, { cwd, signal: controller.signal });
return { stdout: result.stdout || "", success: true };
} catch (error) {
const err = error as { name?: string; code?: string; message?: string };
const isTimeout = err?.name === "AbortError" || err?.code === "ABORT_ERR";
if (isTimeout) {
return { stdout: "", success: false, timeout: true };
}
return { stdout: err?.message || String(error), success: false };
} finally {
clearTimeout(timeoutId);
}
}
export async function syncRepository(pi: ExtensionAPI, settings: MemoryMdSettings): Promise<SyncResult> {
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
const { repoUrl } = settings;
if (!repoUrl) {
return { success: false, message: "Git repository URL or local path not configured" };
}
const repoName = getRepoName(settings);
if (fs.existsSync(localPath)) {
const project = getProjectMeta(localPath);
if (project.gitRoot !== project.cwd) {
return { success: false, message: `Directory exists but is not a git repo: ${localPath}` };
}
if (await isAlreadyAtUpstream(pi, localPath)) {
return { success: true, message: `[${repoName}] is already latest`, updated: false };
}
const pullResult = await gitExec(pi, localPath, ["pull", "--rebase", "--autostash"]);
if (pullResult.timeout) return { success: false, message: TIMEOUT_MESSAGE };
if (!pullResult.success) return { success: false, message: pullResult.stdout || "Pull failed" };
const updated = pullResult.stdout.includes("Updating") || pullResult.stdout.includes("Fast-forward");
return {
success: true,
message: updated ? `Pulled latest changes from [${repoName}]` : `[${repoName}] is already latest`,
updated,
};
}
fs.mkdirSync(localPath, { recursive: true });
const memoryDirName = path.basename(localPath);
const parentDir = path.dirname(localPath);
const cloneResult = await gitExec(pi, parentDir, ["clone", repoUrl, memoryDirName]);
if (cloneResult.timeout) return { success: false, message: TIMEOUT_MESSAGE };
if (cloneResult.success) {
return { success: true, message: `Cloned [${repoName}] successfully`, updated: true };
}
return { success: false, message: cloneResult.stdout || "Clone failed" };
}
export async function pushRepository(pi: ExtensionAPI, settings: MemoryMdSettings): Promise<SyncResult> {
const localPath = settings.localPath ?? DEFAULT_LOCAL_PATH;
const { repoUrl } = settings;
if (!repoUrl) {
return { success: false, message: "Git repository URL or local path not configured" };
}
const project = getProjectMeta(localPath);
if (project.gitRoot !== project.cwd) {
return { success: false, message: `Git repository not initialized: ${localPath}` };
}
const repoName = getRepoName(settings);
const statusResult = await gitExec(pi, localPath, ["status", "--porcelain"]);
if (!statusResult.success) {
return { success: false, message: statusResult.stdout || "Git status failed" };
}
const hasChanges = statusResult.stdout.trim().length > 0;
if (hasChanges) {
const addResult = await gitExec(pi, localPath, ["add", "."]);
if (!addResult.success) {
return { success: false, message: addResult.stdout || "Git add failed" };
}
const timestamp = formatCommitTimestamp();
const commitResult = await gitExec(pi, localPath, ["commit", "-m", `Update memory - ${timestamp}`]);
if (!commitResult.success) {
return { success: false, message: commitResult.stdout || "Commit failed" };
}
}
if (!hasChanges && !(await hasCommitsToPush(pi, localPath))) {
return { success: true, message: `[${repoName}] already up to date`, updated: false };
}
const pushResult = await gitExec(pi, localPath, ["push"]);
if (pushResult.timeout) {
return { success: false, message: TIMEOUT_MESSAGE };
}
if (!pushResult.success) {
return { success: false, message: pushResult.stdout || "Push failed" };
}
return {
success: true,
message: hasChanges ? `Committed and pushed changes to [${repoName}]` : `[${repoName}] already up to date`,
updated: hasChanges,
};
}