@aigne/afs-git 1.11.0-beta.1 → 1.11.0-beta.10
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/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.mjs +7 -0
- package/dist/index.cjs +1216 -291
- package/dist/index.d.cts +361 -216
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +361 -216
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1216 -292
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
const require_decorate = require('./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs');
|
|
1
3
|
let node_child_process = require("node:child_process");
|
|
2
4
|
let node_crypto = require("node:crypto");
|
|
3
5
|
let node_fs_promises = require("node:fs/promises");
|
|
4
6
|
let node_os = require("node:os");
|
|
5
7
|
let node_path = require("node:path");
|
|
6
8
|
let node_util = require("node:util");
|
|
9
|
+
let _aigne_afs = require("@aigne/afs");
|
|
10
|
+
let _aigne_afs_provider = require("@aigne/afs/provider");
|
|
7
11
|
let _aigne_afs_utils_zod = require("@aigne/afs/utils/zod");
|
|
8
12
|
let simple_git = require("simple-git");
|
|
9
13
|
let zod = require("zod");
|
|
@@ -30,18 +34,35 @@ const afsGitOptionsSchema = (0, _aigne_afs_utils_zod.camelize)(zod.z.object({
|
|
|
30
34
|
password: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string())
|
|
31
35
|
})) }))
|
|
32
36
|
}).refine((data) => data.repoPath || data.remoteUrl, { message: "Either repoPath or remoteUrl must be provided" }));
|
|
33
|
-
var AFSGit = class AFSGit {
|
|
37
|
+
var AFSGit = class AFSGit extends _aigne_afs_provider.AFSBaseProvider {
|
|
34
38
|
static schema() {
|
|
35
39
|
return afsGitOptionsSchema;
|
|
36
40
|
}
|
|
37
|
-
static
|
|
41
|
+
static manifest() {
|
|
42
|
+
return {
|
|
43
|
+
name: "git",
|
|
44
|
+
description: "Git repository browser with branch-based access.\n- Browse branches, read files at any ref, search across repository\n- Exec actions (readwrite): `diff`, `create-branch`, `commit`, `merge`\n- Virtual `.log/` tree exposes commit history per branch\n- Path structure: `/{branch}/{file-path}` (branch `/` encoded as `~`)",
|
|
45
|
+
uriTemplate: "git://{localPath+}",
|
|
46
|
+
category: "version-control",
|
|
47
|
+
schema: zod.z.object({
|
|
48
|
+
localPath: zod.z.string(),
|
|
49
|
+
branch: zod.z.string().optional(),
|
|
50
|
+
remoteUrl: zod.z.string().optional()
|
|
51
|
+
}),
|
|
52
|
+
tags: ["git", "version-control"]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
static async load({ basePath, config } = {}) {
|
|
38
56
|
const instance = new AFSGit({
|
|
39
|
-
...await AFSGit.schema().parseAsync(
|
|
40
|
-
cwd:
|
|
57
|
+
...await AFSGit.schema().parseAsync(config),
|
|
58
|
+
cwd: basePath
|
|
41
59
|
});
|
|
42
60
|
await instance.ready();
|
|
43
61
|
return instance;
|
|
44
62
|
}
|
|
63
|
+
name;
|
|
64
|
+
description;
|
|
65
|
+
accessMode;
|
|
45
66
|
initPromise;
|
|
46
67
|
git;
|
|
47
68
|
tempBase;
|
|
@@ -51,7 +72,10 @@ var AFSGit = class AFSGit {
|
|
|
51
72
|
clonedPath;
|
|
52
73
|
repoPath;
|
|
53
74
|
constructor(options) {
|
|
75
|
+
super();
|
|
54
76
|
this.options = options;
|
|
77
|
+
if (options.localPath && !options.repoPath) options.repoPath = options.localPath;
|
|
78
|
+
if (options.branch && !options.branches) options.branches = [options.branch];
|
|
55
79
|
(0, _aigne_afs_utils_zod.zodParse)(afsGitOptionsSchema, options);
|
|
56
80
|
let repoPath;
|
|
57
81
|
let repoName;
|
|
@@ -64,6 +88,15 @@ var AFSGit = class AFSGit {
|
|
|
64
88
|
const repoHash = (0, node_crypto.createHash)("md5").update(options.remoteUrl).digest("hex").substring(0, 8);
|
|
65
89
|
repoPath = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-remote-${repoHash}`);
|
|
66
90
|
} else throw new Error("Either repoPath or remoteUrl must be provided");
|
|
91
|
+
if (options.repoPath && !options.remoteUrl) {
|
|
92
|
+
const { existsSync, mkdirSync } = require("node:fs");
|
|
93
|
+
const { execSync } = require("node:child_process");
|
|
94
|
+
if (!existsSync(repoPath)) mkdirSync(repoPath, { recursive: true });
|
|
95
|
+
if (!existsSync((0, node_path.join)(repoPath, ".git"))) execSync("git init -b main", {
|
|
96
|
+
cwd: repoPath,
|
|
97
|
+
stdio: "ignore"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
67
100
|
this.repoPath = repoPath;
|
|
68
101
|
this.name = options.name || repoName;
|
|
69
102
|
this.description = options.description;
|
|
@@ -88,7 +121,18 @@ var AFSGit = class AFSGit {
|
|
|
88
121
|
if (options.remoteUrl) {
|
|
89
122
|
const targetPath = options.repoPath ? (0, node_path.isAbsolute)(options.repoPath) ? options.repoPath : (0, node_path.join)(options.cwd || process.cwd(), options.repoPath) : this.repoPath;
|
|
90
123
|
if (!options.repoPath) this.isAutoCloned = true;
|
|
91
|
-
|
|
124
|
+
const exists = await (0, node_fs_promises.stat)(targetPath).then(() => true).catch(() => false);
|
|
125
|
+
let needsClone = !exists;
|
|
126
|
+
if (exists) {
|
|
127
|
+
if (!await (0, simple_git.simpleGit)(targetPath).checkIsRepo().catch(() => false)) {
|
|
128
|
+
await (0, node_fs_promises.rm)(targetPath, {
|
|
129
|
+
recursive: true,
|
|
130
|
+
force: true
|
|
131
|
+
});
|
|
132
|
+
needsClone = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (needsClone) {
|
|
92
136
|
const singleBranch = options.branches?.length === 1 ? options.branches[0] : void 0;
|
|
93
137
|
await AFSGit.cloneRepository(options.remoteUrl, targetPath, {
|
|
94
138
|
depth: options.depth ?? 1,
|
|
@@ -104,6 +148,7 @@ var AFSGit = class AFSGit {
|
|
|
104
148
|
this.clonedPath = this.isAutoCloned ? targetPath : void 0;
|
|
105
149
|
}
|
|
106
150
|
this.git = (0, simple_git.simpleGit)(this.repoPath);
|
|
151
|
+
if (!await this.git.checkIsRepo()) throw new Error(`Not a git repository: ${this.repoPath}`);
|
|
107
152
|
}
|
|
108
153
|
/**
|
|
109
154
|
* Clone a remote repository to local path
|
|
@@ -124,290 +169,234 @@ var AFSGit = class AFSGit {
|
|
|
124
169
|
}
|
|
125
170
|
await git.clone(cloneUrl, targetPath, cloneArgs);
|
|
126
171
|
}
|
|
127
|
-
name;
|
|
128
|
-
description;
|
|
129
|
-
accessMode;
|
|
130
172
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* Examples:
|
|
134
|
-
* "/" -> { branch: undefined, filePath: "" }
|
|
135
|
-
* "/main" -> { branch: "main", filePath: "" }
|
|
136
|
-
* "/feature~new-feature" -> { branch: "feature/new-feature", filePath: "" }
|
|
137
|
-
* "/main/src/index.ts" -> { branch: "main", filePath: "src/index.ts" }
|
|
173
|
+
* List root (branches)
|
|
174
|
+
* Note: list() returns only children (branches), never the path itself (per new semantics)
|
|
138
175
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
async listRootHandler(ctx) {
|
|
177
|
+
await this.ready();
|
|
178
|
+
const options = ctx.options;
|
|
179
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
180
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
181
|
+
if (maxDepth === 0) return { data: [] };
|
|
182
|
+
const branches = await this.getBranches();
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (const name of branches) {
|
|
185
|
+
if (entries.length >= limit) break;
|
|
186
|
+
const encodedPath = this.buildBranchPath(name);
|
|
187
|
+
const branchChildrenCount = await this.getChildrenCount(name, "");
|
|
188
|
+
entries.push(this.buildEntry(encodedPath, { meta: {
|
|
189
|
+
kind: "git:branch",
|
|
190
|
+
childrenCount: branchChildrenCount
|
|
191
|
+
} }));
|
|
192
|
+
if (maxDepth > 1) {
|
|
193
|
+
const branchResult = await this.listWithGitLsTree(name, "", {
|
|
194
|
+
maxDepth: maxDepth - 1,
|
|
195
|
+
limit: limit - entries.length
|
|
196
|
+
});
|
|
197
|
+
entries.push(...branchResult.data);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { data: entries };
|
|
149
201
|
}
|
|
150
202
|
/**
|
|
151
|
-
*
|
|
203
|
+
* List branch root (matches /main, /develop, etc.)
|
|
152
204
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
gif: "image/gif",
|
|
159
|
-
bmp: "image/bmp",
|
|
160
|
-
webp: "image/webp",
|
|
161
|
-
svg: "image/svg+xml",
|
|
162
|
-
ico: "image/x-icon",
|
|
163
|
-
pdf: "application/pdf",
|
|
164
|
-
txt: "text/plain",
|
|
165
|
-
md: "text/markdown",
|
|
166
|
-
js: "text/javascript",
|
|
167
|
-
ts: "text/typescript",
|
|
168
|
-
json: "application/json",
|
|
169
|
-
html: "text/html",
|
|
170
|
-
css: "text/css",
|
|
171
|
-
xml: "text/xml"
|
|
172
|
-
}[filePath.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
|
|
205
|
+
async listBranchRootHandler(ctx) {
|
|
206
|
+
await this.ready();
|
|
207
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
208
|
+
await this.ensureBranchExists(branch);
|
|
209
|
+
return this.listWithGitLsTree(branch, "", ctx.options);
|
|
173
210
|
}
|
|
174
211
|
/**
|
|
175
|
-
*
|
|
212
|
+
* List files in branch with subpath (matches /main/src, /main/src/foo, etc.)
|
|
176
213
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
"gif",
|
|
184
|
-
"bmp",
|
|
185
|
-
"webp",
|
|
186
|
-
"ico",
|
|
187
|
-
"pdf",
|
|
188
|
-
"zip",
|
|
189
|
-
"tar",
|
|
190
|
-
"gz",
|
|
191
|
-
"exe",
|
|
192
|
-
"dll",
|
|
193
|
-
"so",
|
|
194
|
-
"dylib",
|
|
195
|
-
"wasm"
|
|
196
|
-
].includes(ext || "");
|
|
214
|
+
async listBranchHandler(ctx) {
|
|
215
|
+
await this.ready();
|
|
216
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
217
|
+
await this.ensureBranchExists(branch);
|
|
218
|
+
const filePath = ctx.params.path;
|
|
219
|
+
return this.listWithGitLsTree(branch, filePath, ctx.options);
|
|
197
220
|
}
|
|
198
221
|
/**
|
|
199
|
-
*
|
|
222
|
+
* Read root metadata (introspection only, read-only)
|
|
200
223
|
*/
|
|
201
|
-
async
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return
|
|
224
|
+
async readRootMetaHandler(_ctx) {
|
|
225
|
+
await this.ready();
|
|
226
|
+
const branches = await this.getBranches();
|
|
227
|
+
return this.buildEntry("/.meta", { meta: {
|
|
228
|
+
childrenCount: branches.length,
|
|
229
|
+
type: "root"
|
|
230
|
+
} });
|
|
205
231
|
}
|
|
206
232
|
/**
|
|
207
|
-
*
|
|
233
|
+
* Read branch root metadata (introspection only, read-only)
|
|
208
234
|
*/
|
|
209
|
-
async
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
worktreePath,
|
|
222
|
-
branch
|
|
223
|
-
]);
|
|
224
|
-
}
|
|
225
|
-
this.worktrees.set(branch, worktreePath);
|
|
226
|
-
return worktreePath;
|
|
235
|
+
async readBranchMetaHandler(ctx) {
|
|
236
|
+
await this.ready();
|
|
237
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
238
|
+
await this.ensureBranchExists(branch);
|
|
239
|
+
const childrenCount = await this.getChildrenCount(branch, "");
|
|
240
|
+
const metaPath = `${`/${this.encodeBranchName(branch)}`}/.meta`;
|
|
241
|
+
const lastCommit = await this.getLastCommit(branch);
|
|
242
|
+
return this.buildEntry(metaPath, { meta: {
|
|
243
|
+
childrenCount,
|
|
244
|
+
type: "branch",
|
|
245
|
+
lastCommit
|
|
246
|
+
} });
|
|
227
247
|
}
|
|
228
248
|
/**
|
|
229
|
-
*
|
|
249
|
+
* Read file or directory metadata in branch (introspection only, read-only)
|
|
230
250
|
*/
|
|
231
|
-
async
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
entries.push({
|
|
252
|
-
id: afsPath,
|
|
253
|
-
path: afsPath,
|
|
254
|
-
metadata: {
|
|
255
|
-
type: "file",
|
|
256
|
-
size
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
return { data: entries };
|
|
260
|
-
}
|
|
261
|
-
const queue = [{
|
|
262
|
-
path: targetPath,
|
|
263
|
-
depth: 0
|
|
264
|
-
}];
|
|
265
|
-
while (queue.length > 0) {
|
|
266
|
-
const { path: itemPath, depth } = queue.shift();
|
|
267
|
-
const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
|
|
268
|
-
const lines = (await this.git.raw([
|
|
269
|
-
"ls-tree",
|
|
270
|
-
"-l",
|
|
271
|
-
itemTreeish
|
|
272
|
-
])).split("\n").filter((line) => line.trim()).slice(0, limit - entries.length);
|
|
273
|
-
for (const line of lines) {
|
|
274
|
-
const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
|
|
275
|
-
if (!match) continue;
|
|
276
|
-
const type = match[2];
|
|
277
|
-
const sizeStr = match[4];
|
|
278
|
-
const name = match[5];
|
|
279
|
-
const isDirectory = type === "tree";
|
|
280
|
-
const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
|
|
281
|
-
const fullPath = itemPath ? `${itemPath}/${name}` : name;
|
|
282
|
-
const afsPath = this.buildPath(branch, fullPath);
|
|
283
|
-
entries.push({
|
|
284
|
-
id: afsPath,
|
|
285
|
-
path: afsPath,
|
|
286
|
-
metadata: {
|
|
287
|
-
type: isDirectory ? "directory" : "file",
|
|
288
|
-
size
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
if (isDirectory && depth + 1 < maxDepth) queue.push({
|
|
292
|
-
path: fullPath,
|
|
293
|
-
depth: depth + 1
|
|
294
|
-
});
|
|
295
|
-
if (entries.length >= limit) return { data: entries };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return { data: entries };
|
|
299
|
-
} catch (error) {
|
|
300
|
-
return {
|
|
301
|
-
data: [],
|
|
302
|
-
message: error.message
|
|
303
|
-
};
|
|
304
|
-
}
|
|
251
|
+
async readPathMetaHandler(ctx) {
|
|
252
|
+
await this.ready();
|
|
253
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
254
|
+
await this.ensureBranchExists(branch);
|
|
255
|
+
const filePath = ctx.params.path;
|
|
256
|
+
const objectType = await this.git.raw([
|
|
257
|
+
"cat-file",
|
|
258
|
+
"-t",
|
|
259
|
+
`${branch}:${filePath}`
|
|
260
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
261
|
+
if (objectType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
262
|
+
const isDir = objectType === "tree";
|
|
263
|
+
const metaPath = `/${this.encodeBranchName(branch)}/${filePath}/.meta`;
|
|
264
|
+
let childrenCount;
|
|
265
|
+
if (isDir) childrenCount = await this.getChildrenCount(branch, filePath);
|
|
266
|
+
return this.buildEntry(metaPath, { meta: {
|
|
267
|
+
childrenCount,
|
|
268
|
+
type: isDir ? "directory" : "file",
|
|
269
|
+
gitObjectType: objectType
|
|
270
|
+
} });
|
|
305
271
|
}
|
|
306
272
|
/**
|
|
307
|
-
*
|
|
308
|
-
* Branch names with slashes are encoded by replacing / with ~
|
|
309
|
-
* @param branch Branch name (may contain slashes)
|
|
310
|
-
* @param filePath File path within branch
|
|
273
|
+
* Read root
|
|
311
274
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
275
|
+
async readRootHandler(_ctx) {
|
|
276
|
+
await this.ready();
|
|
277
|
+
const branches = await this.getBranches();
|
|
278
|
+
return this.buildEntry("/", { meta: { childrenCount: branches.length } });
|
|
316
279
|
}
|
|
317
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Read branch root
|
|
282
|
+
*/
|
|
283
|
+
async readBranchRootHandler(ctx) {
|
|
318
284
|
await this.ready();
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
return this.listWithGitLsTree(branch, filePath, options);
|
|
285
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
286
|
+
await this.ensureBranchExists(branch);
|
|
287
|
+
const branchPath = this.buildBranchPath(branch);
|
|
288
|
+
const childrenCount = await this.getChildrenCount(branch, "");
|
|
289
|
+
const lastCommit = await this.getLastCommit(branch);
|
|
290
|
+
return this.buildEntry(branchPath, { meta: {
|
|
291
|
+
childrenCount,
|
|
292
|
+
lastCommit
|
|
293
|
+
} });
|
|
329
294
|
}
|
|
330
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Read file or directory in branch
|
|
297
|
+
*/
|
|
298
|
+
async readBranchHandler(ctx) {
|
|
331
299
|
await this.ready();
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
metadata: { type: "directory" }
|
|
344
|
-
} };
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
if (await this.git.raw([
|
|
348
|
-
"cat-file",
|
|
349
|
-
"-t",
|
|
350
|
-
`${branch}:${filePath}`
|
|
351
|
-
]).then((t) => t.trim()) === "tree") {
|
|
352
|
-
const afsPath$1 = this.buildPath(branch, filePath);
|
|
353
|
-
return { data: {
|
|
354
|
-
id: afsPath$1,
|
|
355
|
-
path: afsPath$1,
|
|
356
|
-
metadata: { type: "directory" }
|
|
357
|
-
} };
|
|
300
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
301
|
+
await this.ensureBranchExists(branch);
|
|
302
|
+
const filePath = ctx.params.path;
|
|
303
|
+
const worktreePath = this.worktrees.get(branch);
|
|
304
|
+
if (worktreePath) try {
|
|
305
|
+
const fullPath = (0, node_path.join)(worktreePath, filePath);
|
|
306
|
+
const stats = await (0, node_fs_promises.stat)(fullPath);
|
|
307
|
+
if (stats.isDirectory()) {
|
|
308
|
+
const files = await (0, node_fs_promises.readdir)(fullPath);
|
|
309
|
+
const afsPath$2 = this.buildBranchPath(branch, filePath);
|
|
310
|
+
return this.buildEntry(afsPath$2, { meta: { childrenCount: files.length } });
|
|
358
311
|
}
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const isBinary = this.isBinaryFile(filePath);
|
|
366
|
-
let content;
|
|
367
|
-
const metadata = {
|
|
368
|
-
type: "file",
|
|
369
|
-
size,
|
|
370
|
-
mimeType
|
|
371
|
-
};
|
|
372
|
-
if (isBinary) {
|
|
373
|
-
const { stdout } = await execFileAsync("git", [
|
|
374
|
-
"cat-file",
|
|
375
|
-
"-p",
|
|
376
|
-
`${branch}:${filePath}`
|
|
377
|
-
], {
|
|
378
|
-
cwd: this.options.repoPath,
|
|
379
|
-
encoding: "buffer",
|
|
380
|
-
maxBuffer: 10 * 1024 * 1024
|
|
381
|
-
});
|
|
382
|
-
content = stdout.toString("base64");
|
|
383
|
-
metadata.contentType = "base64";
|
|
384
|
-
} else content = await this.git.show([`${branch}:${filePath}`]);
|
|
385
|
-
const afsPath = this.buildPath(branch, filePath);
|
|
386
|
-
return { data: {
|
|
387
|
-
id: afsPath,
|
|
388
|
-
path: afsPath,
|
|
389
|
-
content,
|
|
390
|
-
metadata
|
|
391
|
-
} };
|
|
392
|
-
} catch (error) {
|
|
393
|
-
return {
|
|
394
|
-
data: void 0,
|
|
395
|
-
message: error.message
|
|
312
|
+
const mimeType$1 = this.getMimeType(filePath);
|
|
313
|
+
const isBinary$1 = this.isBinaryFile(filePath);
|
|
314
|
+
let content$1;
|
|
315
|
+
const meta$1 = {
|
|
316
|
+
size: stats.size,
|
|
317
|
+
mimeType: mimeType$1
|
|
396
318
|
};
|
|
319
|
+
if (isBinary$1) {
|
|
320
|
+
content$1 = (await (0, node_fs_promises.readFile)(fullPath)).toString("base64");
|
|
321
|
+
meta$1.contentType = "base64";
|
|
322
|
+
} else content$1 = await (0, node_fs_promises.readFile)(fullPath, "utf8");
|
|
323
|
+
const afsPath$1 = this.buildBranchPath(branch, filePath);
|
|
324
|
+
return this.buildEntry(afsPath$1, {
|
|
325
|
+
content: content$1,
|
|
326
|
+
meta: meta$1,
|
|
327
|
+
createdAt: stats.birthtime,
|
|
328
|
+
updatedAt: stats.mtime
|
|
329
|
+
});
|
|
330
|
+
} catch {}
|
|
331
|
+
const objectType = await this.git.raw([
|
|
332
|
+
"cat-file",
|
|
333
|
+
"-t",
|
|
334
|
+
`${branch}:${filePath}`
|
|
335
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
336
|
+
if (objectType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
337
|
+
if (objectType === "tree") {
|
|
338
|
+
const afsPath$1 = this.buildBranchPath(branch, filePath);
|
|
339
|
+
const childrenCount = await this.getChildrenCount(branch, filePath);
|
|
340
|
+
return this.buildEntry(afsPath$1, { meta: { childrenCount } });
|
|
397
341
|
}
|
|
342
|
+
const size = await this.git.raw([
|
|
343
|
+
"cat-file",
|
|
344
|
+
"-s",
|
|
345
|
+
`${branch}:${filePath}`
|
|
346
|
+
]).then((s) => Number.parseInt(s.trim(), 10));
|
|
347
|
+
const mimeType = this.getMimeType(filePath);
|
|
348
|
+
const isBinary = this.isBinaryFile(filePath);
|
|
349
|
+
let content;
|
|
350
|
+
const meta = {
|
|
351
|
+
size,
|
|
352
|
+
mimeType
|
|
353
|
+
};
|
|
354
|
+
if (isBinary) {
|
|
355
|
+
const { stdout } = await execFileAsync("git", [
|
|
356
|
+
"cat-file",
|
|
357
|
+
"-p",
|
|
358
|
+
`${branch}:${filePath}`
|
|
359
|
+
], {
|
|
360
|
+
cwd: this.options.repoPath,
|
|
361
|
+
encoding: "buffer",
|
|
362
|
+
maxBuffer: 10 * 1024 * 1024
|
|
363
|
+
});
|
|
364
|
+
content = stdout.toString("base64");
|
|
365
|
+
meta.contentType = "base64";
|
|
366
|
+
} else content = await this.git.show([`${branch}:${filePath}`]);
|
|
367
|
+
const afsPath = this.buildBranchPath(branch, filePath);
|
|
368
|
+
return this.buildEntry(afsPath, {
|
|
369
|
+
content,
|
|
370
|
+
meta
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Write to root is not allowed
|
|
375
|
+
*/
|
|
376
|
+
async writeRootHandler() {
|
|
377
|
+
throw new Error("Cannot write to root");
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Write to branch root is not allowed
|
|
381
|
+
*/
|
|
382
|
+
async writeBranchRootHandler() {
|
|
383
|
+
throw new Error("Cannot write to branch root");
|
|
398
384
|
}
|
|
399
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Write file in branch
|
|
387
|
+
*/
|
|
388
|
+
async writeHandler(ctx, payload) {
|
|
400
389
|
await this.ready();
|
|
401
|
-
const
|
|
402
|
-
|
|
390
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
391
|
+
const filePath = ctx.params.path;
|
|
392
|
+
const append = ctx.options?.append ?? false;
|
|
403
393
|
const worktreePath = await this.ensureWorktree(branch);
|
|
404
394
|
const fullPath = (0, node_path.join)(worktreePath, filePath);
|
|
405
|
-
const append = options?.append ?? false;
|
|
406
395
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
407
|
-
if (
|
|
396
|
+
if (payload.content !== void 0) {
|
|
408
397
|
let contentToWrite;
|
|
409
|
-
if (typeof
|
|
410
|
-
else contentToWrite = JSON.stringify(
|
|
398
|
+
if (typeof payload.content === "string") contentToWrite = payload.content;
|
|
399
|
+
else contentToWrite = JSON.stringify(payload.content, null, 2);
|
|
411
400
|
await (0, node_fs_promises.writeFile)(fullPath, contentToWrite, {
|
|
412
401
|
encoding: "utf8",
|
|
413
402
|
flag: append ? "a" : "w"
|
|
@@ -423,32 +412,56 @@ var AFSGit = class AFSGit {
|
|
|
423
412
|
await gitInstance.commit(`Update ${filePath}`);
|
|
424
413
|
}
|
|
425
414
|
const stats = await (0, node_fs_promises.stat)(fullPath);
|
|
426
|
-
const afsPath = this.
|
|
415
|
+
const afsPath = this.buildBranchPath(branch, filePath);
|
|
427
416
|
return { data: {
|
|
428
417
|
id: afsPath,
|
|
429
418
|
path: afsPath,
|
|
430
|
-
content:
|
|
431
|
-
summary:
|
|
419
|
+
content: payload.content,
|
|
420
|
+
summary: payload.summary,
|
|
432
421
|
createdAt: stats.birthtime,
|
|
433
422
|
updatedAt: stats.mtime,
|
|
434
|
-
|
|
435
|
-
...
|
|
436
|
-
type: stats.isDirectory() ? "directory" : "file",
|
|
423
|
+
meta: {
|
|
424
|
+
...payload.meta,
|
|
437
425
|
size: stats.size
|
|
438
426
|
},
|
|
439
|
-
userId:
|
|
440
|
-
sessionId:
|
|
441
|
-
linkTo:
|
|
427
|
+
userId: payload.userId,
|
|
428
|
+
sessionId: payload.sessionId,
|
|
429
|
+
linkTo: payload.linkTo
|
|
442
430
|
} };
|
|
443
431
|
}
|
|
444
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Delete root is not allowed
|
|
434
|
+
*/
|
|
435
|
+
async deleteRootHandler() {
|
|
436
|
+
throw new Error("Cannot delete root");
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Delete branch root is not allowed
|
|
440
|
+
*/
|
|
441
|
+
async deleteBranchRootHandler(ctx) {
|
|
442
|
+
await this.ready();
|
|
443
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
444
|
+
await this.ensureBranchExists(branch);
|
|
445
|
+
throw new Error("Cannot delete branch root");
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Delete file in branch
|
|
449
|
+
*/
|
|
450
|
+
async deleteHandler(ctx) {
|
|
445
451
|
await this.ready();
|
|
446
|
-
const
|
|
447
|
-
|
|
452
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
453
|
+
const filePath = ctx.params.path;
|
|
454
|
+
const recursive = ctx.options?.recursive ?? false;
|
|
448
455
|
const worktreePath = await this.ensureWorktree(branch);
|
|
449
456
|
const fullPath = (0, node_path.join)(worktreePath, filePath);
|
|
450
|
-
|
|
451
|
-
|
|
457
|
+
let stats;
|
|
458
|
+
try {
|
|
459
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error.code === "ENOENT") throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
if (stats.isDirectory() && !recursive) throw new Error(`Cannot delete directory '/${ctx.params.branch}/${filePath}' without recursive option. Set recursive: true to delete directories.`);
|
|
452
465
|
await (0, node_fs_promises.rm)(fullPath, {
|
|
453
466
|
recursive,
|
|
454
467
|
force: true
|
|
@@ -462,20 +475,28 @@ var AFSGit = class AFSGit {
|
|
|
462
475
|
}
|
|
463
476
|
await gitInstance.commit(`Delete ${filePath}`);
|
|
464
477
|
}
|
|
465
|
-
return { message: `Successfully deleted:
|
|
478
|
+
return { message: `Successfully deleted: /${ctx.params.branch}/${filePath}` };
|
|
466
479
|
}
|
|
467
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Rename file in branch
|
|
482
|
+
*/
|
|
483
|
+
async renameHandler(ctx, newPath) {
|
|
468
484
|
await this.ready();
|
|
469
|
-
const
|
|
485
|
+
const oldBranch = this.decodeBranchName(ctx.params.branch);
|
|
486
|
+
const oldFilePath = ctx.params.path;
|
|
470
487
|
const { branch: newBranch, filePath: newFilePath } = this.parsePath(newPath);
|
|
471
|
-
|
|
488
|
+
const overwrite = ctx.options?.overwrite ?? false;
|
|
472
489
|
if (!newBranch || !newFilePath) throw new Error("Cannot rename to root or branch root");
|
|
473
490
|
if (oldBranch !== newBranch) throw new Error("Cannot rename across branches");
|
|
474
491
|
const worktreePath = await this.ensureWorktree(oldBranch);
|
|
475
492
|
const oldFullPath = (0, node_path.join)(worktreePath, oldFilePath);
|
|
476
493
|
const newFullPath = (0, node_path.join)(worktreePath, newFilePath);
|
|
477
|
-
|
|
478
|
-
|
|
494
|
+
try {
|
|
495
|
+
await (0, node_fs_promises.stat)(oldFullPath);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error.code === "ENOENT") throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(oldBranch, oldFilePath));
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
479
500
|
try {
|
|
480
501
|
await (0, node_fs_promises.stat)(newFullPath);
|
|
481
502
|
if (!overwrite) throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
|
|
@@ -493,15 +514,26 @@ var AFSGit = class AFSGit {
|
|
|
493
514
|
}
|
|
494
515
|
await gitInstance.commit(`Rename ${oldFilePath} to ${newFilePath}`);
|
|
495
516
|
}
|
|
496
|
-
return { message: `Successfully renamed '
|
|
517
|
+
return { message: `Successfully renamed '/${ctx.params.branch}/${oldFilePath}' to '${newPath}'` };
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Search files in branch root
|
|
521
|
+
*/
|
|
522
|
+
async searchBranchRootHandler(ctx, query, options) {
|
|
523
|
+
return this.searchInBranch(ctx.params.branch, "", query, options);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Search files in branch path
|
|
527
|
+
*/
|
|
528
|
+
async searchHandler(ctx, query, options) {
|
|
529
|
+
return this.searchInBranch(ctx.params.branch, ctx.params.path, query, options);
|
|
497
530
|
}
|
|
498
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Internal search implementation
|
|
533
|
+
*/
|
|
534
|
+
async searchInBranch(encodedBranch, filePath, query, options) {
|
|
499
535
|
await this.ready();
|
|
500
|
-
const
|
|
501
|
-
if (!branch) return {
|
|
502
|
-
data: [],
|
|
503
|
-
message: "Search requires a branch path"
|
|
504
|
-
};
|
|
536
|
+
const branch = this.decodeBranchName(encodedBranch);
|
|
505
537
|
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
506
538
|
try {
|
|
507
539
|
const args = [
|
|
@@ -531,15 +563,12 @@ var AFSGit = class AFSGit {
|
|
|
531
563
|
lineNum = matchNoBranch[2];
|
|
532
564
|
content = matchNoBranch[3];
|
|
533
565
|
}
|
|
534
|
-
const afsPath = this.
|
|
566
|
+
const afsPath = this.buildBranchPath(branch, matchPath);
|
|
535
567
|
if (processedFiles.has(afsPath)) continue;
|
|
536
568
|
processedFiles.add(afsPath);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
summary: `Line ${lineNum}: ${content}`,
|
|
541
|
-
metadata: { type: "file" }
|
|
542
|
-
});
|
|
569
|
+
const entry = this.buildEntry(afsPath);
|
|
570
|
+
entry.summary = `Line ${lineNum}: ${content}`;
|
|
571
|
+
entries.push(entry);
|
|
543
572
|
if (entries.length >= limit) break;
|
|
544
573
|
}
|
|
545
574
|
return {
|
|
@@ -555,6 +584,867 @@ var AFSGit = class AFSGit {
|
|
|
555
584
|
}
|
|
556
585
|
}
|
|
557
586
|
/**
|
|
587
|
+
* Stat root
|
|
588
|
+
*/
|
|
589
|
+
async statRootHandler(_ctx) {
|
|
590
|
+
const entry = await this.readRootHandler(_ctx);
|
|
591
|
+
if (!entry) return { data: void 0 };
|
|
592
|
+
const { content: _content, ...rest } = entry;
|
|
593
|
+
return { data: rest };
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Stat branch root
|
|
597
|
+
*/
|
|
598
|
+
async statBranchRootHandler(ctx) {
|
|
599
|
+
const entry = await this.readBranchRootHandler(ctx);
|
|
600
|
+
if (!entry) return { data: void 0 };
|
|
601
|
+
const { content: _content, ...rest } = entry;
|
|
602
|
+
return { data: rest };
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Stat file or directory in branch
|
|
606
|
+
*/
|
|
607
|
+
async statHandler(ctx) {
|
|
608
|
+
const entry = await this.readBranchHandler(ctx);
|
|
609
|
+
if (!entry) return { data: void 0 };
|
|
610
|
+
const { content: _content, ...rest } = entry;
|
|
611
|
+
return { data: rest };
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Explain root → repo info, branch list, default branch
|
|
615
|
+
*/
|
|
616
|
+
async explainRootHandler(_ctx) {
|
|
617
|
+
await this.ready();
|
|
618
|
+
const format = _ctx.options?.format || "markdown";
|
|
619
|
+
const branches = await this.getBranches();
|
|
620
|
+
const currentBranch = await this.git.revparse(["--abbrev-ref", "HEAD"]).then((b) => b.trim());
|
|
621
|
+
let remoteUrl;
|
|
622
|
+
try {
|
|
623
|
+
remoteUrl = await this.git.remote(["get-url", "origin"]).then((u) => u?.trim());
|
|
624
|
+
} catch {}
|
|
625
|
+
const lines = [];
|
|
626
|
+
lines.push("# Git Repository");
|
|
627
|
+
lines.push("");
|
|
628
|
+
lines.push(`**Provider:** ${this.name}`);
|
|
629
|
+
if (this.description) lines.push(`**Description:** ${this.description}`);
|
|
630
|
+
lines.push(`**Default Branch:** ${currentBranch}`);
|
|
631
|
+
if (remoteUrl) lines.push(`**Remote:** ${remoteUrl}`);
|
|
632
|
+
lines.push(`**Branches:** ${branches.length}`);
|
|
633
|
+
lines.push("");
|
|
634
|
+
lines.push("## Branches");
|
|
635
|
+
lines.push("");
|
|
636
|
+
for (const branch of branches) lines.push(`- ${branch}`);
|
|
637
|
+
return {
|
|
638
|
+
content: lines.join("\n"),
|
|
639
|
+
format
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Explain branch → branch name, HEAD commit, file count
|
|
644
|
+
*/
|
|
645
|
+
async explainBranchHandler(ctx) {
|
|
646
|
+
await this.ready();
|
|
647
|
+
const format = ctx.options?.format || "markdown";
|
|
648
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
649
|
+
await this.ensureBranchExists(branch);
|
|
650
|
+
const lastCommit = await this.getLastCommit(branch);
|
|
651
|
+
const fileCount = await this.getTreeFileCount(branch, "");
|
|
652
|
+
const lines = [];
|
|
653
|
+
lines.push(`# Branch: ${branch}`);
|
|
654
|
+
lines.push("");
|
|
655
|
+
lines.push(`**HEAD Commit:** ${lastCommit.shortHash} - ${lastCommit.message}`);
|
|
656
|
+
lines.push(`**Author:** ${lastCommit.author}`);
|
|
657
|
+
lines.push(`**Date:** ${lastCommit.date}`);
|
|
658
|
+
lines.push(`**Files:** ${fileCount} entries in tree`);
|
|
659
|
+
return {
|
|
660
|
+
content: lines.join("\n"),
|
|
661
|
+
format
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Explain file or directory → path, size, last modified commit
|
|
666
|
+
*/
|
|
667
|
+
async explainPathHandler(ctx) {
|
|
668
|
+
await this.ready();
|
|
669
|
+
const format = ctx.options?.format || "markdown";
|
|
670
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
671
|
+
await this.ensureBranchExists(branch);
|
|
672
|
+
const filePath = ctx.params.path;
|
|
673
|
+
const objectType = await this.git.raw([
|
|
674
|
+
"cat-file",
|
|
675
|
+
"-t",
|
|
676
|
+
`${branch}:${filePath}`
|
|
677
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
678
|
+
if (objectType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
679
|
+
const isDir = objectType === "tree";
|
|
680
|
+
const lines = [];
|
|
681
|
+
lines.push(`# ${(0, node_path.basename)(filePath)}`);
|
|
682
|
+
lines.push("");
|
|
683
|
+
lines.push(`**Path:** ${filePath}`);
|
|
684
|
+
lines.push(`**Type:** ${isDir ? "directory" : "file"}`);
|
|
685
|
+
if (!isDir) {
|
|
686
|
+
const size = await this.git.raw([
|
|
687
|
+
"cat-file",
|
|
688
|
+
"-s",
|
|
689
|
+
`${branch}:${filePath}`
|
|
690
|
+
]).then((s) => Number.parseInt(s.trim(), 10));
|
|
691
|
+
lines.push(`**Size:** ${size} bytes`);
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const logLines = (await this.git.raw([
|
|
695
|
+
"log",
|
|
696
|
+
"-1",
|
|
697
|
+
"--format=%H%n%h%n%an%n%aI%n%s",
|
|
698
|
+
branch,
|
|
699
|
+
"--",
|
|
700
|
+
filePath
|
|
701
|
+
])).trim().split("\n");
|
|
702
|
+
if (logLines.length >= 5) {
|
|
703
|
+
lines.push("");
|
|
704
|
+
lines.push("## Last Modified");
|
|
705
|
+
lines.push(`**Commit:** ${logLines[1]} - ${logLines[4]}`);
|
|
706
|
+
lines.push(`**Author:** ${logLines[2]}`);
|
|
707
|
+
lines.push(`**Date:** ${logLines[3]}`);
|
|
708
|
+
}
|
|
709
|
+
} catch {}
|
|
710
|
+
return {
|
|
711
|
+
content: lines.join("\n"),
|
|
712
|
+
format
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async readCapabilitiesHandler(_ctx) {
|
|
716
|
+
const operations = [
|
|
717
|
+
"list",
|
|
718
|
+
"read",
|
|
719
|
+
"stat",
|
|
720
|
+
"explain",
|
|
721
|
+
"search"
|
|
722
|
+
];
|
|
723
|
+
if (this.accessMode === "readwrite") operations.push("write", "delete", "rename");
|
|
724
|
+
const actionCatalogs = [];
|
|
725
|
+
if (this.accessMode === "readwrite") actionCatalogs.push({
|
|
726
|
+
description: "Git workflow actions",
|
|
727
|
+
catalog: [
|
|
728
|
+
{
|
|
729
|
+
name: "diff",
|
|
730
|
+
description: "Compare two branches or refs",
|
|
731
|
+
inputSchema: {
|
|
732
|
+
type: "object",
|
|
733
|
+
properties: {
|
|
734
|
+
from: {
|
|
735
|
+
type: "string",
|
|
736
|
+
description: "Source ref"
|
|
737
|
+
},
|
|
738
|
+
to: {
|
|
739
|
+
type: "string",
|
|
740
|
+
description: "Target ref"
|
|
741
|
+
},
|
|
742
|
+
path: {
|
|
743
|
+
type: "string",
|
|
744
|
+
description: "Optional path filter"
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
required: ["from", "to"]
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
name: "create-branch",
|
|
752
|
+
description: "Create a new branch",
|
|
753
|
+
inputSchema: {
|
|
754
|
+
type: "object",
|
|
755
|
+
properties: {
|
|
756
|
+
name: {
|
|
757
|
+
type: "string",
|
|
758
|
+
description: "New branch name"
|
|
759
|
+
},
|
|
760
|
+
from: {
|
|
761
|
+
type: "string",
|
|
762
|
+
description: "Source ref (defaults to current HEAD)"
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
required: ["name"]
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "commit",
|
|
770
|
+
description: "Commit staged changes",
|
|
771
|
+
inputSchema: {
|
|
772
|
+
type: "object",
|
|
773
|
+
properties: {
|
|
774
|
+
message: {
|
|
775
|
+
type: "string",
|
|
776
|
+
description: "Commit message"
|
|
777
|
+
},
|
|
778
|
+
author: {
|
|
779
|
+
type: "object",
|
|
780
|
+
properties: {
|
|
781
|
+
name: { type: "string" },
|
|
782
|
+
email: { type: "string" }
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
required: ["message"]
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "merge",
|
|
791
|
+
description: "Merge a branch into the current branch",
|
|
792
|
+
inputSchema: {
|
|
793
|
+
type: "object",
|
|
794
|
+
properties: {
|
|
795
|
+
branch: {
|
|
796
|
+
type: "string",
|
|
797
|
+
description: "Branch to merge"
|
|
798
|
+
},
|
|
799
|
+
message: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: "Custom merge message"
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
required: ["branch"]
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
],
|
|
808
|
+
discovery: {
|
|
809
|
+
pathTemplate: "/:branch/.actions",
|
|
810
|
+
note: "Git workflow actions (readwrite mode only)"
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
return {
|
|
814
|
+
id: "/.meta/.capabilities",
|
|
815
|
+
path: "/.meta/.capabilities",
|
|
816
|
+
content: {
|
|
817
|
+
schemaVersion: 1,
|
|
818
|
+
provider: this.name,
|
|
819
|
+
description: this.description || "Git repository provider",
|
|
820
|
+
tools: [],
|
|
821
|
+
actions: actionCatalogs,
|
|
822
|
+
operations: this.getOperationsDeclaration()
|
|
823
|
+
},
|
|
824
|
+
meta: {
|
|
825
|
+
kind: "afs:capabilities",
|
|
826
|
+
operations
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* List available actions for a branch
|
|
832
|
+
*/
|
|
833
|
+
async listBranchActions(ctx) {
|
|
834
|
+
if (this.accessMode !== "readwrite") return { data: [] };
|
|
835
|
+
const basePath = `/${ctx.params.branch}/.actions`;
|
|
836
|
+
return { data: [
|
|
837
|
+
{
|
|
838
|
+
id: "diff",
|
|
839
|
+
path: `${basePath}/diff`,
|
|
840
|
+
summary: "Compare two branches or refs",
|
|
841
|
+
meta: {
|
|
842
|
+
kind: "afs:executable",
|
|
843
|
+
kinds: ["afs:executable", "afs:node"],
|
|
844
|
+
inputSchema: {
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
from: {
|
|
848
|
+
type: "string",
|
|
849
|
+
description: "Source ref"
|
|
850
|
+
},
|
|
851
|
+
to: {
|
|
852
|
+
type: "string",
|
|
853
|
+
description: "Target ref"
|
|
854
|
+
},
|
|
855
|
+
path: {
|
|
856
|
+
type: "string",
|
|
857
|
+
description: "Optional path filter"
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
required: ["from", "to"]
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
id: "create-branch",
|
|
866
|
+
path: `${basePath}/create-branch`,
|
|
867
|
+
summary: "Create a new branch from this ref",
|
|
868
|
+
meta: {
|
|
869
|
+
kind: "afs:executable",
|
|
870
|
+
kinds: ["afs:executable", "afs:node"],
|
|
871
|
+
inputSchema: {
|
|
872
|
+
type: "object",
|
|
873
|
+
properties: {
|
|
874
|
+
name: {
|
|
875
|
+
type: "string",
|
|
876
|
+
description: "New branch name"
|
|
877
|
+
},
|
|
878
|
+
from: {
|
|
879
|
+
type: "string",
|
|
880
|
+
description: "Source ref (defaults to current HEAD)"
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
required: ["name"]
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
id: "commit",
|
|
889
|
+
path: `${basePath}/commit`,
|
|
890
|
+
summary: "Commit staged changes",
|
|
891
|
+
meta: {
|
|
892
|
+
kind: "afs:executable",
|
|
893
|
+
kinds: ["afs:executable", "afs:node"],
|
|
894
|
+
inputSchema: {
|
|
895
|
+
type: "object",
|
|
896
|
+
properties: {
|
|
897
|
+
message: {
|
|
898
|
+
type: "string",
|
|
899
|
+
description: "Commit message"
|
|
900
|
+
},
|
|
901
|
+
author: {
|
|
902
|
+
type: "object",
|
|
903
|
+
properties: {
|
|
904
|
+
name: { type: "string" },
|
|
905
|
+
email: { type: "string" }
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
required: ["message"]
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
id: "merge",
|
|
915
|
+
path: `${basePath}/merge`,
|
|
916
|
+
summary: "Merge another branch into this branch",
|
|
917
|
+
meta: {
|
|
918
|
+
kind: "afs:executable",
|
|
919
|
+
kinds: ["afs:executable", "afs:node"],
|
|
920
|
+
inputSchema: {
|
|
921
|
+
type: "object",
|
|
922
|
+
properties: {
|
|
923
|
+
branch: {
|
|
924
|
+
type: "string",
|
|
925
|
+
description: "Branch to merge"
|
|
926
|
+
},
|
|
927
|
+
message: {
|
|
928
|
+
type: "string",
|
|
929
|
+
description: "Custom merge message"
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
required: ["branch"]
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
] };
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* diff action — compare two branches or refs
|
|
940
|
+
*/
|
|
941
|
+
async diffAction(_ctx, args) {
|
|
942
|
+
await this.ready();
|
|
943
|
+
const from = args.from;
|
|
944
|
+
const to = args.to;
|
|
945
|
+
const pathFilter = args.path;
|
|
946
|
+
if (!from || !to) return {
|
|
947
|
+
success: false,
|
|
948
|
+
error: {
|
|
949
|
+
code: "INVALID_ARGS",
|
|
950
|
+
message: "from and to are required"
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
try {
|
|
954
|
+
const diffArgs = [
|
|
955
|
+
"diff",
|
|
956
|
+
"--stat",
|
|
957
|
+
"--name-only",
|
|
958
|
+
`${from}...${to}`
|
|
959
|
+
];
|
|
960
|
+
if (pathFilter) diffArgs.push("--", pathFilter);
|
|
961
|
+
const files = (await this.git.raw(diffArgs)).trim().split("\n").filter((l) => l.trim()).map((path) => ({ path }));
|
|
962
|
+
const patchArgs = ["diff", `${from}...${to}`];
|
|
963
|
+
if (pathFilter) patchArgs.push("--", pathFilter);
|
|
964
|
+
return {
|
|
965
|
+
success: true,
|
|
966
|
+
data: {
|
|
967
|
+
from,
|
|
968
|
+
to,
|
|
969
|
+
files,
|
|
970
|
+
patch: await this.git.raw(patchArgs),
|
|
971
|
+
filesChanged: files.length
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
} catch (error) {
|
|
975
|
+
return {
|
|
976
|
+
success: false,
|
|
977
|
+
error: {
|
|
978
|
+
code: "DIFF_FAILED",
|
|
979
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* create-branch action — create a new branch
|
|
986
|
+
*/
|
|
987
|
+
async createBranchAction(_ctx, args) {
|
|
988
|
+
await this.ready();
|
|
989
|
+
const name = args.name;
|
|
990
|
+
const from = args.from;
|
|
991
|
+
if (!name) return {
|
|
992
|
+
success: false,
|
|
993
|
+
error: {
|
|
994
|
+
code: "INVALID_ARGS",
|
|
995
|
+
message: "name is required"
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
if (name.includes("..")) return {
|
|
999
|
+
success: false,
|
|
1000
|
+
error: {
|
|
1001
|
+
code: "INVALID_NAME",
|
|
1002
|
+
message: "Branch name contains invalid characters"
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
try {
|
|
1006
|
+
if (from) await this.git.raw([
|
|
1007
|
+
"branch",
|
|
1008
|
+
name,
|
|
1009
|
+
from
|
|
1010
|
+
]);
|
|
1011
|
+
else await this.git.raw(["branch", name]);
|
|
1012
|
+
return {
|
|
1013
|
+
success: true,
|
|
1014
|
+
data: {
|
|
1015
|
+
branch: name,
|
|
1016
|
+
hash: await this.git.revparse([name]).then((h) => h.trim())
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
return {
|
|
1021
|
+
success: false,
|
|
1022
|
+
error: {
|
|
1023
|
+
code: "CREATE_BRANCH_FAILED",
|
|
1024
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* commit action — commit staged changes
|
|
1031
|
+
*/
|
|
1032
|
+
async commitAction(_ctx, args) {
|
|
1033
|
+
await this.ready();
|
|
1034
|
+
const message = args.message;
|
|
1035
|
+
if (!message) return {
|
|
1036
|
+
success: false,
|
|
1037
|
+
error: {
|
|
1038
|
+
code: "INVALID_ARGS",
|
|
1039
|
+
message: "message is required"
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
const author = args.author;
|
|
1043
|
+
try {
|
|
1044
|
+
const git = (0, simple_git.simpleGit)(this.repoPath);
|
|
1045
|
+
const status = await git.status();
|
|
1046
|
+
if (status.staged.length === 0 && status.files.filter((f) => f.index !== " " && f.index !== "?").length === 0) return {
|
|
1047
|
+
success: false,
|
|
1048
|
+
error: {
|
|
1049
|
+
code: "NO_CHANGES",
|
|
1050
|
+
message: "No staged changes to commit"
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
if (author?.name) await git.addConfig("user.name", author.name, void 0, "local");
|
|
1054
|
+
if (author?.email) await git.addConfig("user.email", author.email, void 0, "local");
|
|
1055
|
+
const result = await git.commit(message);
|
|
1056
|
+
return {
|
|
1057
|
+
success: true,
|
|
1058
|
+
data: {
|
|
1059
|
+
hash: result.commit || "",
|
|
1060
|
+
message,
|
|
1061
|
+
filesChanged: result.summary.changes
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
return {
|
|
1066
|
+
success: false,
|
|
1067
|
+
error: {
|
|
1068
|
+
code: "COMMIT_FAILED",
|
|
1069
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* merge action — merge a branch into current branch
|
|
1076
|
+
*/
|
|
1077
|
+
async mergeAction(_ctx, args) {
|
|
1078
|
+
await this.ready();
|
|
1079
|
+
const branch = args.branch;
|
|
1080
|
+
if (!branch) return {
|
|
1081
|
+
success: false,
|
|
1082
|
+
error: {
|
|
1083
|
+
code: "INVALID_ARGS",
|
|
1084
|
+
message: "branch is required"
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
const customMessage = args.message;
|
|
1088
|
+
try {
|
|
1089
|
+
const git = (0, simple_git.simpleGit)(this.repoPath);
|
|
1090
|
+
if (!(await git.branchLocal()).all.includes(branch)) return {
|
|
1091
|
+
success: false,
|
|
1092
|
+
error: {
|
|
1093
|
+
code: "BRANCH_NOT_FOUND",
|
|
1094
|
+
message: `Branch '${branch}' not found`
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const mergeArgs = [branch];
|
|
1098
|
+
if (customMessage) mergeArgs.push("-m", customMessage);
|
|
1099
|
+
const result = await git.merge(mergeArgs);
|
|
1100
|
+
return {
|
|
1101
|
+
success: true,
|
|
1102
|
+
data: {
|
|
1103
|
+
hash: await git.revparse(["HEAD"]).then((h) => h.trim()),
|
|
1104
|
+
merged: branch,
|
|
1105
|
+
conflicts: result.conflicts || []
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
try {
|
|
1110
|
+
await (0, simple_git.simpleGit)(this.repoPath).merge(["--abort"]);
|
|
1111
|
+
} catch {}
|
|
1112
|
+
return {
|
|
1113
|
+
success: false,
|
|
1114
|
+
error: {
|
|
1115
|
+
code: "MERGE_FAILED",
|
|
1116
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* List .log/ → commit list with pagination
|
|
1123
|
+
*/
|
|
1124
|
+
async listLogHandler(ctx) {
|
|
1125
|
+
await this.ready();
|
|
1126
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
1127
|
+
await this.ensureBranchExists(branch);
|
|
1128
|
+
const options = ctx.options;
|
|
1129
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
1130
|
+
const offset = options?.offset || 0;
|
|
1131
|
+
const commits = await this.getCommitList(branch, limit, offset);
|
|
1132
|
+
const branchEncoded = this.encodeBranchName(branch);
|
|
1133
|
+
return { data: commits.map((commit, i) => this.buildEntry(`/${branchEncoded}/.log/${offset + i}`, { meta: {
|
|
1134
|
+
hash: commit.hash,
|
|
1135
|
+
shortHash: commit.shortHash,
|
|
1136
|
+
author: commit.author,
|
|
1137
|
+
date: commit.date,
|
|
1138
|
+
message: commit.message
|
|
1139
|
+
} })) };
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Read .log/{index} → commit diff/patch content
|
|
1143
|
+
*/
|
|
1144
|
+
async readLogEntryHandler(ctx) {
|
|
1145
|
+
await this.ready();
|
|
1146
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
1147
|
+
await this.ensureBranchExists(branch);
|
|
1148
|
+
const index = Number.parseInt(ctx.params.index, 10);
|
|
1149
|
+
if (Number.isNaN(index) || index < 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${ctx.params.index}`);
|
|
1150
|
+
const commits = await this.getCommitList(branch, 1, index);
|
|
1151
|
+
if (commits.length === 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${index}`);
|
|
1152
|
+
const commit = commits[0];
|
|
1153
|
+
let diff;
|
|
1154
|
+
try {
|
|
1155
|
+
diff = await this.git.raw([
|
|
1156
|
+
"show",
|
|
1157
|
+
"--stat",
|
|
1158
|
+
"--patch",
|
|
1159
|
+
commit.hash
|
|
1160
|
+
]);
|
|
1161
|
+
} catch {
|
|
1162
|
+
diff = "";
|
|
1163
|
+
}
|
|
1164
|
+
const branchEncoded = this.encodeBranchName(branch);
|
|
1165
|
+
return this.buildEntry(`/${branchEncoded}/.log/${index}`, {
|
|
1166
|
+
content: diff,
|
|
1167
|
+
meta: {
|
|
1168
|
+
hash: commit.hash,
|
|
1169
|
+
shortHash: commit.shortHash,
|
|
1170
|
+
author: commit.author,
|
|
1171
|
+
date: commit.date,
|
|
1172
|
+
message: commit.message
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Read .log/{index}/.meta → commit metadata only (no diff)
|
|
1178
|
+
*/
|
|
1179
|
+
async readLogEntryMetaHandler(ctx) {
|
|
1180
|
+
await this.ready();
|
|
1181
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
1182
|
+
await this.ensureBranchExists(branch);
|
|
1183
|
+
const index = Number.parseInt(ctx.params.index, 10);
|
|
1184
|
+
if (Number.isNaN(index) || index < 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${ctx.params.index}/.meta`);
|
|
1185
|
+
const commits = await this.getCommitList(branch, 1, index);
|
|
1186
|
+
if (commits.length === 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${index}/.meta`);
|
|
1187
|
+
const commit = commits[0];
|
|
1188
|
+
const branchEncoded = this.encodeBranchName(branch);
|
|
1189
|
+
return this.buildEntry(`/${branchEncoded}/.log/${index}/.meta`, { meta: {
|
|
1190
|
+
hash: commit.hash,
|
|
1191
|
+
shortHash: commit.shortHash,
|
|
1192
|
+
author: commit.author,
|
|
1193
|
+
date: commit.date,
|
|
1194
|
+
message: commit.message
|
|
1195
|
+
} });
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Decode branch name (replace ~ with /)
|
|
1199
|
+
*/
|
|
1200
|
+
decodeBranchName(encoded) {
|
|
1201
|
+
return encoded.replace(/~/g, "/");
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Encode branch name (replace / with ~)
|
|
1205
|
+
*/
|
|
1206
|
+
encodeBranchName(branch) {
|
|
1207
|
+
return branch.replace(/\//g, "~");
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Parse AFS path into branch and file path
|
|
1211
|
+
* Branch names may contain slashes and are encoded with ~ in paths
|
|
1212
|
+
*/
|
|
1213
|
+
parsePath(path) {
|
|
1214
|
+
const segments = (0, node_path.join)("/", path).split("/").filter(Boolean);
|
|
1215
|
+
if (segments.length === 0) return {
|
|
1216
|
+
branch: void 0,
|
|
1217
|
+
filePath: ""
|
|
1218
|
+
};
|
|
1219
|
+
return {
|
|
1220
|
+
branch: segments[0].replace(/~/g, "/"),
|
|
1221
|
+
filePath: segments.slice(1).join("/")
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Build AFS path with encoded branch name
|
|
1226
|
+
* Branch names with slashes are encoded by replacing / with ~
|
|
1227
|
+
*/
|
|
1228
|
+
buildBranchPath(branch, filePath) {
|
|
1229
|
+
const encodedBranch = this.encodeBranchName(branch);
|
|
1230
|
+
if (!filePath) return `/${encodedBranch}`;
|
|
1231
|
+
return `/${encodedBranch}/${filePath}`;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Get list of available branches
|
|
1235
|
+
*/
|
|
1236
|
+
async getBranches() {
|
|
1237
|
+
const allBranches = (await this.git.branchLocal()).all;
|
|
1238
|
+
if (this.options.branches && this.options.branches.length > 0) return allBranches.filter((branch) => this.options.branches.includes(branch));
|
|
1239
|
+
return allBranches;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Check if a branch exists, throw AFSNotFoundError if not
|
|
1243
|
+
*/
|
|
1244
|
+
async ensureBranchExists(branch) {
|
|
1245
|
+
if (!(await this.getBranches()).includes(branch)) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch));
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Get the number of children for a tree (directory) in git
|
|
1249
|
+
*/
|
|
1250
|
+
async getChildrenCount(branch, path) {
|
|
1251
|
+
try {
|
|
1252
|
+
const treeish = path ? `${branch}:${path}` : branch;
|
|
1253
|
+
return (await this.git.raw(["ls-tree", treeish])).split("\n").filter((line) => line.trim()).length;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return 0;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Get the last commit on a branch
|
|
1260
|
+
*/
|
|
1261
|
+
async getLastCommit(branch) {
|
|
1262
|
+
const lines = (await this.git.raw([
|
|
1263
|
+
"log",
|
|
1264
|
+
"-1",
|
|
1265
|
+
"--format=%H%n%h%n%an%n%aI%n%s",
|
|
1266
|
+
branch
|
|
1267
|
+
])).trim().split("\n");
|
|
1268
|
+
return {
|
|
1269
|
+
hash: lines[0] || "",
|
|
1270
|
+
shortHash: lines[1] || "",
|
|
1271
|
+
author: lines[2] || "",
|
|
1272
|
+
date: lines[3] || "",
|
|
1273
|
+
message: lines[4] || ""
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Count total files in a tree (recursively)
|
|
1278
|
+
*/
|
|
1279
|
+
async getTreeFileCount(branch, path) {
|
|
1280
|
+
try {
|
|
1281
|
+
const treeish = path ? `${branch}:${path}` : branch;
|
|
1282
|
+
return (await this.git.raw([
|
|
1283
|
+
"ls-tree",
|
|
1284
|
+
"-r",
|
|
1285
|
+
treeish
|
|
1286
|
+
])).split("\n").filter((line) => line.trim()).length;
|
|
1287
|
+
} catch {
|
|
1288
|
+
return 0;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get a list of commits on a branch with limit/offset
|
|
1293
|
+
*/
|
|
1294
|
+
async getCommitList(branch, limit, offset) {
|
|
1295
|
+
try {
|
|
1296
|
+
const args = [
|
|
1297
|
+
"log",
|
|
1298
|
+
`--skip=${offset}`,
|
|
1299
|
+
`-${limit}`,
|
|
1300
|
+
"--format=%H%n%h%n%an%n%aI%n%s%n---COMMIT_SEP---",
|
|
1301
|
+
branch
|
|
1302
|
+
];
|
|
1303
|
+
return (await this.git.raw(args)).split("---COMMIT_SEP---").filter((b) => b.trim()).map((block) => {
|
|
1304
|
+
const lines = block.trim().split("\n");
|
|
1305
|
+
return {
|
|
1306
|
+
hash: lines[0] || "",
|
|
1307
|
+
shortHash: lines[1] || "",
|
|
1308
|
+
author: lines[2] || "",
|
|
1309
|
+
date: lines[3] || "",
|
|
1310
|
+
message: lines[4] || ""
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
} catch {
|
|
1314
|
+
return [];
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Ensure worktree exists for a branch (lazy creation)
|
|
1319
|
+
*/
|
|
1320
|
+
async ensureWorktree(branch) {
|
|
1321
|
+
if (this.worktrees.has(branch)) return this.worktrees.get(branch);
|
|
1322
|
+
if ((await this.git.revparse(["--abbrev-ref", "HEAD"])).trim() === branch) {
|
|
1323
|
+
this.worktrees.set(branch, this.repoPath);
|
|
1324
|
+
return this.repoPath;
|
|
1325
|
+
}
|
|
1326
|
+
const worktreePath = (0, node_path.join)(this.tempBase, branch);
|
|
1327
|
+
if (!await (0, node_fs_promises.stat)(worktreePath).then(() => true).catch(() => false)) {
|
|
1328
|
+
await (0, node_fs_promises.mkdir)(this.tempBase, { recursive: true });
|
|
1329
|
+
await this.git.raw([
|
|
1330
|
+
"worktree",
|
|
1331
|
+
"add",
|
|
1332
|
+
worktreePath,
|
|
1333
|
+
branch
|
|
1334
|
+
]);
|
|
1335
|
+
}
|
|
1336
|
+
this.worktrees.set(branch, worktreePath);
|
|
1337
|
+
return worktreePath;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* List files using git ls-tree (no worktree needed)
|
|
1341
|
+
* Note: list() returns only children, never the path itself (per new semantics)
|
|
1342
|
+
*/
|
|
1343
|
+
async listWithGitLsTree(branch, path, options) {
|
|
1344
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
1345
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
1346
|
+
const entries = [];
|
|
1347
|
+
const targetPath = path || "";
|
|
1348
|
+
const treeish = targetPath ? `${branch}:${targetPath}` : branch;
|
|
1349
|
+
try {
|
|
1350
|
+
const pathType = await this.git.raw([
|
|
1351
|
+
"cat-file",
|
|
1352
|
+
"-t",
|
|
1353
|
+
treeish
|
|
1354
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
1355
|
+
if (pathType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, path));
|
|
1356
|
+
if (pathType === "blob") return { data: [] };
|
|
1357
|
+
if (maxDepth === 0) return { data: [] };
|
|
1358
|
+
const queue = [{
|
|
1359
|
+
path: targetPath,
|
|
1360
|
+
depth: 0
|
|
1361
|
+
}];
|
|
1362
|
+
while (queue.length > 0) {
|
|
1363
|
+
const { path: itemPath, depth } = queue.shift();
|
|
1364
|
+
const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
|
|
1365
|
+
const lines = (await this.git.raw([
|
|
1366
|
+
"ls-tree",
|
|
1367
|
+
"-l",
|
|
1368
|
+
itemTreeish
|
|
1369
|
+
])).split("\n").filter((line) => line.trim()).slice(0, limit - entries.length);
|
|
1370
|
+
for (const line of lines) {
|
|
1371
|
+
const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
|
|
1372
|
+
if (!match) continue;
|
|
1373
|
+
const type = match[2];
|
|
1374
|
+
const sizeStr = match[4];
|
|
1375
|
+
const name = match[5];
|
|
1376
|
+
const isDirectory = type === "tree";
|
|
1377
|
+
const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
|
|
1378
|
+
const fullPath = itemPath ? `${itemPath}/${name}` : name;
|
|
1379
|
+
const afsPath = this.buildBranchPath(branch, fullPath);
|
|
1380
|
+
const childrenCount = isDirectory ? await this.getChildrenCount(branch, fullPath) : void 0;
|
|
1381
|
+
entries.push(this.buildEntry(afsPath, { meta: {
|
|
1382
|
+
kind: isDirectory ? "git:directory" : "git:file",
|
|
1383
|
+
size,
|
|
1384
|
+
childrenCount
|
|
1385
|
+
} }));
|
|
1386
|
+
if (isDirectory && depth + 1 < maxDepth) queue.push({
|
|
1387
|
+
path: fullPath,
|
|
1388
|
+
depth: depth + 1
|
|
1389
|
+
});
|
|
1390
|
+
if (entries.length >= limit) return { data: entries };
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return { data: entries };
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
if (error instanceof _aigne_afs.AFSNotFoundError) throw error;
|
|
1396
|
+
throw new Error(`Failed to list: ${error.message}`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Detect MIME type based on file extension
|
|
1401
|
+
*/
|
|
1402
|
+
getMimeType(filePath) {
|
|
1403
|
+
return {
|
|
1404
|
+
png: "image/png",
|
|
1405
|
+
jpg: "image/jpeg",
|
|
1406
|
+
jpeg: "image/jpeg",
|
|
1407
|
+
gif: "image/gif",
|
|
1408
|
+
bmp: "image/bmp",
|
|
1409
|
+
webp: "image/webp",
|
|
1410
|
+
svg: "image/svg+xml",
|
|
1411
|
+
ico: "image/x-icon",
|
|
1412
|
+
pdf: "application/pdf",
|
|
1413
|
+
txt: "text/plain",
|
|
1414
|
+
md: "text/markdown",
|
|
1415
|
+
js: "text/javascript",
|
|
1416
|
+
ts: "text/typescript",
|
|
1417
|
+
json: "application/json",
|
|
1418
|
+
html: "text/html",
|
|
1419
|
+
css: "text/css",
|
|
1420
|
+
xml: "text/xml"
|
|
1421
|
+
}[filePath.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Check if file is likely binary based on extension
|
|
1425
|
+
*/
|
|
1426
|
+
isBinaryFile(filePath) {
|
|
1427
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
1428
|
+
return [
|
|
1429
|
+
"png",
|
|
1430
|
+
"jpg",
|
|
1431
|
+
"jpeg",
|
|
1432
|
+
"gif",
|
|
1433
|
+
"bmp",
|
|
1434
|
+
"webp",
|
|
1435
|
+
"ico",
|
|
1436
|
+
"pdf",
|
|
1437
|
+
"zip",
|
|
1438
|
+
"tar",
|
|
1439
|
+
"gz",
|
|
1440
|
+
"exe",
|
|
1441
|
+
"dll",
|
|
1442
|
+
"so",
|
|
1443
|
+
"dylib",
|
|
1444
|
+
"wasm"
|
|
1445
|
+
].includes(ext || "");
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
558
1448
|
* Fetch latest changes from remote
|
|
559
1449
|
*/
|
|
560
1450
|
async fetch() {
|
|
@@ -605,6 +1495,41 @@ var AFSGit = class AFSGit {
|
|
|
605
1495
|
} catch {}
|
|
606
1496
|
}
|
|
607
1497
|
};
|
|
1498
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/", { handleDepth: true })], AFSGit.prototype, "listRootHandler", null);
|
|
1499
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/:branch", { handleDepth: true })], AFSGit.prototype, "listBranchRootHandler", null);
|
|
1500
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/:branch/:path+", { handleDepth: true })], AFSGit.prototype, "listBranchHandler", null);
|
|
1501
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Meta)("/")], AFSGit.prototype, "readRootMetaHandler", null);
|
|
1502
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Meta)("/:branch")], AFSGit.prototype, "readBranchMetaHandler", null);
|
|
1503
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Meta)("/:branch/:path+")], AFSGit.prototype, "readPathMetaHandler", null);
|
|
1504
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/")], AFSGit.prototype, "readRootHandler", null);
|
|
1505
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch")], AFSGit.prototype, "readBranchRootHandler", null);
|
|
1506
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch/:path+")], AFSGit.prototype, "readBranchHandler", null);
|
|
1507
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Write)("/")], AFSGit.prototype, "writeRootHandler", null);
|
|
1508
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Write)("/:branch")], AFSGit.prototype, "writeBranchRootHandler", null);
|
|
1509
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Write)("/:branch/:path+")], AFSGit.prototype, "writeHandler", null);
|
|
1510
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Delete)("/")], AFSGit.prototype, "deleteRootHandler", null);
|
|
1511
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Delete)("/:branch")], AFSGit.prototype, "deleteBranchRootHandler", null);
|
|
1512
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Delete)("/:branch/:path+")], AFSGit.prototype, "deleteHandler", null);
|
|
1513
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Rename)("/:branch/:path+")], AFSGit.prototype, "renameHandler", null);
|
|
1514
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Search)("/:branch")], AFSGit.prototype, "searchBranchRootHandler", null);
|
|
1515
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Search)("/:branch/:path+")], AFSGit.prototype, "searchHandler", null);
|
|
1516
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Stat)("/")], AFSGit.prototype, "statRootHandler", null);
|
|
1517
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Stat)("/:branch")], AFSGit.prototype, "statBranchRootHandler", null);
|
|
1518
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Stat)("/:branch/:path+")], AFSGit.prototype, "statHandler", null);
|
|
1519
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Explain)("/")], AFSGit.prototype, "explainRootHandler", null);
|
|
1520
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Explain)("/:branch")], AFSGit.prototype, "explainBranchHandler", null);
|
|
1521
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Explain)("/:branch/:path+")], AFSGit.prototype, "explainPathHandler", null);
|
|
1522
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/.meta/.capabilities")], AFSGit.prototype, "readCapabilitiesHandler", null);
|
|
1523
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Actions)("/:branch")], AFSGit.prototype, "listBranchActions", null);
|
|
1524
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "diff")], AFSGit.prototype, "diffAction", null);
|
|
1525
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "create-branch")], AFSGit.prototype, "createBranchAction", null);
|
|
1526
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "commit")], AFSGit.prototype, "commitAction", null);
|
|
1527
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "merge")], AFSGit.prototype, "mergeAction", null);
|
|
1528
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/:branch/.log")], AFSGit.prototype, "listLogHandler", null);
|
|
1529
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch/.log/:index")], AFSGit.prototype, "readLogEntryHandler", null);
|
|
1530
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch/.log/:index/.meta")], AFSGit.prototype, "readLogEntryMetaHandler", null);
|
|
1531
|
+
var src_default = AFSGit;
|
|
608
1532
|
|
|
609
1533
|
//#endregion
|
|
610
|
-
exports.AFSGit = AFSGit;
|
|
1534
|
+
exports.AFSGit = AFSGit;
|
|
1535
|
+
exports.default = src_default;
|