@canyonjs/git-provider 0.0.3 → 0.0.6
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/README.md +9 -9
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +107 -55
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -62,15 +62,15 @@ GitHub 仅 token、`base` 固定为 `https://api.github.com`。
|
|
|
62
62
|
|
|
63
63
|
## 开发与脚本
|
|
64
64
|
|
|
65
|
-
| 脚本 | 说明
|
|
66
|
-
| -------------------- |
|
|
67
|
-
| `pnpm run build` | 使用 tsdown 产出 `dist`
|
|
68
|
-
| `pnpm run dev` | 监听构建
|
|
69
|
-
| `pnpm run typecheck` | TypeScript 检查
|
|
70
|
-
| `pnpm run test` | Vitest
|
|
71
|
-
| `pnpm run debug` | 本地脚本 `
|
|
72
|
-
|
|
73
|
-
目录概览:**`src/adapter.ts`** 定义 **`ScmAdapter`**;**`src/gitlab.ts`** / **`src/github.ts`** 为实现;**`src/diff-line.ts`** 为行号计算与扩展名判定;**`src/types.ts`**
|
|
65
|
+
| 脚本 | 说明 |
|
|
66
|
+
| -------------------- | ---------------------------------------------------------- |
|
|
67
|
+
| `pnpm run build` | 使用 tsdown 产出 `dist` |
|
|
68
|
+
| `pnpm run dev` | 监听构建 |
|
|
69
|
+
| `pnpm run typecheck` | TypeScript 检查 |
|
|
70
|
+
| `pnpm run test` | Vitest |
|
|
71
|
+
| `pnpm run debug` | 本地脚本 `scripts/debug/`(`.env` 配置 token、仓库与 ref) |
|
|
72
|
+
|
|
73
|
+
目录概览:**`src/adapter.ts`** 定义 **`ScmAdapter`**;**`src/gitlab.ts`** / **`src/github.ts`** 为实现;**`src/diff-line.ts`** 为行号计算与扩展名判定;**`src/types.ts`** 为配置与各数据结构类型。**`tests/`** 为 Vitest 测试;**`tests/test-utils/`** 为本机 HTTP mock、fixture JSON 等(不进入 `dist`)。
|
|
74
74
|
|
|
75
75
|
## 许可证
|
|
76
76
|
|
package/dist/index.d.mts
CHANGED
|
@@ -34,8 +34,15 @@ interface Compare {
|
|
|
34
34
|
interface ScmAdapter {
|
|
35
35
|
/** 获取仓库信息(id、pathWithNamespace、description) */
|
|
36
36
|
getRepoInfo(repoID: string): Promise<RepoInfo>;
|
|
37
|
+
/** 获取 base..head 之间 commit sha 列表(顺序与 GitLab `repository/compare` 返回的 `commits` 一致;无 `commits` 时可能仅有 `commit.id`) */
|
|
38
|
+
getCommitsBetween(repoID: string, base: string, head: string): Promise<string[]>;
|
|
37
39
|
/** 获取 base...head 之间变更 */
|
|
38
40
|
getCompare(repoID: string, base: string, head: string): Promise<Compare>;
|
|
41
|
+
/**
|
|
42
|
+
* 批量获取指定 ref 下多个文件的源码(通过 archive 下载后解压提取,避免逐文件请求)
|
|
43
|
+
* @returns Map<相对路径, 文件内容 UTF-8 字符串>
|
|
44
|
+
*/
|
|
45
|
+
getSourceFiles(repoID: string, sha: string, filePaths: string[]): Promise<Map<string, string>>;
|
|
39
46
|
}
|
|
40
47
|
//#endregion
|
|
41
48
|
//#region src/index.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import axios from "axios";
|
|
1
2
|
import { diffLines } from "diff";
|
|
3
|
+
/** 统一超时;需要改签名单独在调用里传 `AxiosRequestConfig` */
|
|
4
|
+
const http = axios.create({ timeout: 1e4 });
|
|
2
5
|
var HttpError = class extends Error {
|
|
3
6
|
status;
|
|
4
7
|
statusText;
|
|
@@ -12,52 +15,24 @@ var HttpError = class extends Error {
|
|
|
12
15
|
this.data = data;
|
|
13
16
|
}
|
|
14
17
|
};
|
|
15
|
-
function
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return timeoutSignal;
|
|
18
|
+
function rethrowAsHttpError(e) {
|
|
19
|
+
if (axios.isAxiosError(e) && e.response) throw new HttpError(e.response.status, e.response.statusText ?? "", e.response.data);
|
|
20
|
+
if (e instanceof Error) throw e;
|
|
21
|
+
throw new Error(String(e));
|
|
20
22
|
}
|
|
21
|
-
async function
|
|
22
|
-
const text = await res.text();
|
|
23
|
-
if (!text) return void 0;
|
|
24
|
-
if ((res.headers.get("content-type") ?? "").includes("application/json")) try {
|
|
25
|
-
return JSON.parse(text);
|
|
26
|
-
} catch {
|
|
27
|
-
return text;
|
|
28
|
-
}
|
|
29
|
-
return text;
|
|
30
|
-
}
|
|
31
|
-
async function dispatch(url, config) {
|
|
32
|
-
const timeoutMs = config.timeout ?? 1e4;
|
|
33
|
-
const controller = new AbortController();
|
|
34
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
35
|
-
const signal = mergeSignal(controller.signal, config.signal);
|
|
23
|
+
async function get(url, config) {
|
|
36
24
|
try {
|
|
37
|
-
const
|
|
38
|
-
method: config.method ?? "GET",
|
|
39
|
-
headers: config.headers,
|
|
40
|
-
body: config.body,
|
|
41
|
-
signal
|
|
42
|
-
});
|
|
43
|
-
const data = await parseBody(res);
|
|
44
|
-
if (!res.ok) throw new HttpError(res.status, res.statusText, data);
|
|
25
|
+
const r = await http.get(url, config);
|
|
45
26
|
return {
|
|
46
|
-
data,
|
|
47
|
-
status:
|
|
48
|
-
statusText:
|
|
27
|
+
data: r.data,
|
|
28
|
+
status: r.status,
|
|
29
|
+
statusText: r.statusText
|
|
49
30
|
};
|
|
50
|
-
}
|
|
51
|
-
|
|
31
|
+
} catch (e) {
|
|
32
|
+
rethrowAsHttpError(e);
|
|
52
33
|
}
|
|
53
34
|
}
|
|
54
|
-
|
|
55
|
-
function get(url, config = {}) {
|
|
56
|
-
return dispatch(url, {
|
|
57
|
-
...config,
|
|
58
|
-
method: "GET"
|
|
59
|
-
});
|
|
60
|
-
}
|
|
35
|
+
http.defaults;
|
|
61
36
|
//#endregion
|
|
62
37
|
//#region src/github.ts
|
|
63
38
|
const GITHUB_BASE = "https://api.github.com";
|
|
@@ -92,9 +67,15 @@ var GithubAdapter = class {
|
|
|
92
67
|
description: data.description ?? ""
|
|
93
68
|
};
|
|
94
69
|
}
|
|
70
|
+
async getCommitsBetween(_repoID, _base, _head) {
|
|
71
|
+
throw new Error("GitHubAdapter.getCommitsBetween 尚未实现");
|
|
72
|
+
}
|
|
95
73
|
async getCompare(_repoID, _base, _head) {
|
|
96
74
|
throw new Error("GitHubAdapter.getCompare 尚未实现");
|
|
97
75
|
}
|
|
76
|
+
async getSourceFiles(repoID, sha, filePaths) {
|
|
77
|
+
throw new Error("GitHubAdapter.getSourceFiles 尚未实现");
|
|
78
|
+
}
|
|
98
79
|
};
|
|
99
80
|
//#endregion
|
|
100
81
|
//#region src/diff-line.ts
|
|
@@ -187,6 +168,15 @@ async function computeLineBucketsByRef(files, baseRef, headRef, fetchText, concu
|
|
|
187
168
|
}
|
|
188
169
|
//#endregion
|
|
189
170
|
//#region src/gitlab.ts
|
|
171
|
+
/** 唯一路径数超过该值时用 archive.zip;否则并发请求单文件 raw,避免为少量路径拉整仓 */
|
|
172
|
+
const SOURCE_FILES_ARCHIVE_THRESHOLD = 8;
|
|
173
|
+
/** 提交 id 列表:完全以 compare 响应为准——有 `commits` 则用其顺序;否则用 `commit.id`(同 ref / 无区间时) */
|
|
174
|
+
function commitIdsFromCompare(data) {
|
|
175
|
+
const ids = (data.commits ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
176
|
+
if (ids.length > 0) return ids;
|
|
177
|
+
const tip = data.commit?.id;
|
|
178
|
+
return tip != null && tip !== "" ? [tip] : [];
|
|
179
|
+
}
|
|
190
180
|
var GitlabAdapter = class {
|
|
191
181
|
base;
|
|
192
182
|
token;
|
|
@@ -199,39 +189,101 @@ var GitlabAdapter = class {
|
|
|
199
189
|
}
|
|
200
190
|
fileRawUrl(repoID, filePath, ref) {
|
|
201
191
|
const encodedPath = encodeURIComponent(filePath);
|
|
202
|
-
return `${this.base}/
|
|
192
|
+
return `${this.base}/projects/${encodeURIComponent(repoID)}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(ref)}`;
|
|
203
193
|
}
|
|
204
194
|
async getRawFileText(repoID, filePath, ref) {
|
|
205
195
|
const { data } = await get(this.fileRawUrl(repoID, filePath, ref), { headers: this.headers() });
|
|
206
196
|
return typeof data === "string" ? data : "";
|
|
207
197
|
}
|
|
208
198
|
async getRepoInfo(repoID) {
|
|
209
|
-
const { data } = await get(`${this.base}/
|
|
199
|
+
const { data } = await get(`${this.base}/projects/${encodeURIComponent(repoID)}`, { headers: this.headers() });
|
|
210
200
|
return {
|
|
211
201
|
id: String(data.id),
|
|
212
202
|
pathWithNamespace: data.path_with_namespace,
|
|
213
203
|
description: data.description ?? ""
|
|
214
204
|
};
|
|
215
205
|
}
|
|
206
|
+
async fetchComparePayload(repoID, base, head) {
|
|
207
|
+
const { data } = await get(`${this.base}/projects/${encodeURIComponent(repoID)}/repository/compare?from=${encodeURIComponent(base)}&to=${encodeURIComponent(head)}`, { headers: this.headers() });
|
|
208
|
+
return data;
|
|
209
|
+
}
|
|
210
|
+
async getCommitsBetween(repoID, base, head) {
|
|
211
|
+
return commitIdsFromCompare(await this.fetchComparePayload(repoID, base, head));
|
|
212
|
+
}
|
|
216
213
|
async getCompare(repoID, base, head) {
|
|
217
|
-
const
|
|
214
|
+
const data = await this.fetchComparePayload(repoID, base, head);
|
|
215
|
+
const commitList = commitIdsFromCompare(data);
|
|
218
216
|
const diffRows = data.diffs ?? [];
|
|
219
217
|
const comparedPaths = diffRows.map(comparedRefsFromGitlabDiff).filter((entry) => entry != null).filter((entry) => pathMatchesLineDiffExtensions(entry.keyPath));
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
};
|
|
229
|
-
}).filter((item) => pathMatchesLineDiffExtensions(item.path));
|
|
218
|
+
let lineBuckets;
|
|
219
|
+
if (comparedPaths.length === 0 || comparedPaths.length > 1500) lineBuckets = /* @__PURE__ */ new Map();
|
|
220
|
+
else {
|
|
221
|
+
const basePaths = [...new Set(comparedPaths.map((p) => p.pathAtBase))];
|
|
222
|
+
const headPaths = [...new Set(comparedPaths.map((p) => p.pathAtHead))];
|
|
223
|
+
const [baseTexts, headTexts] = await Promise.all([this.getSourceFiles(repoID, base, basePaths), this.getSourceFiles(repoID, head, headPaths)]);
|
|
224
|
+
lineBuckets = await computeLineBucketsByRef(comparedPaths, base, head, (path, ref) => Promise.resolve(ref === base ? baseTexts.get(path) ?? "" : ref === head ? headTexts.get(path) ?? "" : ""));
|
|
225
|
+
}
|
|
230
226
|
return {
|
|
231
|
-
commitList
|
|
232
|
-
changedFiles
|
|
227
|
+
commitList,
|
|
228
|
+
changedFiles: diffRows.map((d) => {
|
|
229
|
+
const path = comparedRefsFromGitlabDiff(d)?.keyPath ?? (d.new_path || d.old_path || "");
|
|
230
|
+
const buckets = lineBuckets.get(path);
|
|
231
|
+
return {
|
|
232
|
+
path,
|
|
233
|
+
additions: buckets?.additions ?? [],
|
|
234
|
+
deletions: buckets?.deletions ?? []
|
|
235
|
+
};
|
|
236
|
+
}).filter((item) => pathMatchesLineDiffExtensions(item.path))
|
|
233
237
|
};
|
|
234
238
|
}
|
|
239
|
+
async getSourceFiles(repoID, sha, filePaths) {
|
|
240
|
+
const uniquePaths = [...new Set(filePaths)];
|
|
241
|
+
const wantAllFiles = uniquePaths.length === 0;
|
|
242
|
+
if (!wantAllFiles && uniquePaths.length <= SOURCE_FILES_ARCHIVE_THRESHOLD) {
|
|
243
|
+
const settled = await Promise.allSettled(uniquePaths.map((path) => this.getRawFileText(repoID, path, sha).then((text) => ({
|
|
244
|
+
path,
|
|
245
|
+
text
|
|
246
|
+
}))));
|
|
247
|
+
const result = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const s of settled) if (s.status === "fulfilled") result.set(s.value.path, s.value.text);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
const pid = encodeURIComponent(repoID);
|
|
252
|
+
const { data } = await get(`${this.base}/projects/${pid}/repository/archive.zip`, {
|
|
253
|
+
headers: this.headers(),
|
|
254
|
+
params: { sha },
|
|
255
|
+
responseType: "arraybuffer",
|
|
256
|
+
timeout: 6e4
|
|
257
|
+
});
|
|
258
|
+
const { default: AdmZip } = await import("adm-zip");
|
|
259
|
+
const { tmpNameSync } = await import("tmp");
|
|
260
|
+
const fs = await import("node:fs");
|
|
261
|
+
const tempZip = tmpNameSync({ postfix: ".zip" });
|
|
262
|
+
try {
|
|
263
|
+
const bin = data;
|
|
264
|
+
const buf = Buffer.isBuffer(bin) ? bin : bin instanceof ArrayBuffer ? Buffer.from(bin) : Buffer.from(bin);
|
|
265
|
+
fs.writeFileSync(tempZip, buf);
|
|
266
|
+
const entries = new AdmZip(tempZip).getEntries();
|
|
267
|
+
const targetSet = wantAllFiles ? null : new Set(uniquePaths);
|
|
268
|
+
const result = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
if (entry.isDirectory) continue;
|
|
271
|
+
const parts = entry.entryName.split("/");
|
|
272
|
+
if (parts.length < 2) continue;
|
|
273
|
+
const relativePath = parts.slice(1).join("/");
|
|
274
|
+
if (targetSet && !targetSet.has(relativePath)) continue;
|
|
275
|
+
try {
|
|
276
|
+
const content = entry.getData().toString("utf8");
|
|
277
|
+
result.set(relativePath, content);
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
} finally {
|
|
282
|
+
try {
|
|
283
|
+
fs.unlinkSync(tempZip);
|
|
284
|
+
} catch {}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
235
287
|
};
|
|
236
288
|
//#endregion
|
|
237
289
|
//#region src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canyonjs/git-provider",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "GitLab provider client with adapter architecture.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -28,11 +28,20 @@
|
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"
|
|
31
|
+
"adm-zip": "^0.5.17",
|
|
32
|
+
"axios": "^1.16.0",
|
|
33
|
+
"diff": "^9.0.0",
|
|
34
|
+
"tmp": "^0.2.5"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
37
|
+
"@canyonjs/report": "^1.0.28",
|
|
38
|
+
"@types/adm-zip": "^0.5.8",
|
|
34
39
|
"@types/node": "^25.5.0",
|
|
40
|
+
"@types/tmp": "^0.2.6",
|
|
35
41
|
"@typescript/native-preview": "7.0.0-dev.20260505.1",
|
|
42
|
+
"@vitest/coverage-istanbul": "4.1.5",
|
|
43
|
+
"@vitest/ui": "4.1.5",
|
|
44
|
+
"dotenv": "^17.4.2",
|
|
36
45
|
"oxfmt": "^0.48.0",
|
|
37
46
|
"oxlint": "^1.63.0",
|
|
38
47
|
"tsdown": "^0.21.7",
|
|
@@ -43,8 +52,9 @@
|
|
|
43
52
|
"build": "tsdown",
|
|
44
53
|
"dev": "tsdown --watch",
|
|
45
54
|
"test": "vitest run",
|
|
55
|
+
"test:ui": "vitest --ui",
|
|
46
56
|
"typecheck": "tsgo --noEmit",
|
|
47
|
-
"debug": "tsx
|
|
57
|
+
"debug": "tsx scripts/debug/index.ts",
|
|
48
58
|
"lint": "oxlint",
|
|
49
59
|
"lint:fix": "oxlint --fix",
|
|
50
60
|
"fmt": "oxfmt",
|