@canyonjs/git-provider 0.0.1

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 ADDED
@@ -0,0 +1,49 @@
1
+ # git-provider
2
+
3
+ 统一封装 GitHub / GitLab / Gitea 的常见仓库 API。
4
+
5
+ ## Development
6
+
7
+ - Install dependencies:
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ - Run the unit tests:
14
+
15
+ ```bash
16
+ npm run test
17
+ ```
18
+
19
+ - Build the library:
20
+
21
+ ```bash
22
+ npm run build
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ts
28
+ import { createGitProviderClient } from 'git-provider'
29
+
30
+ const client = createGitProviderClient({
31
+ provider: 'gitlab', // 'github' | 'gitlab' | 'gitea'
32
+ token: process.env.GIT_TOKEN
33
+ })
34
+
35
+ // 1) 根据 id 或 owner/repo 获取仓库摘要
36
+ const repo = await client.getRepositorySummary('canyon-project/canyon')
37
+
38
+ // 2) 获取 commit 摘要
39
+ const commit = await client.getCommitSummary('canyon-project/canyon', 'abc123')
40
+
41
+ // 3) 获取单个文件内容
42
+ const file = await client.getFileContent('canyon-project/canyon', 'README.md', 'main')
43
+
44
+ // 4) 获取指定 commit 的 zip 产物
45
+ const archive = await client.downloadArchive('canyon-project/canyon', 'abc123')
46
+
47
+ // 5) 比较两个 commit
48
+ const compare = await client.compareCommits('canyon-project/canyon', 'fromSha', 'toSha')
49
+ ```
@@ -0,0 +1,78 @@
1
+ //#region src/index.d.ts
2
+ type GitProvider = 'github' | 'gitlab' | 'gitea';
3
+ interface GitProviderClientOptions {
4
+ provider: GitProvider;
5
+ token?: string;
6
+ baseUrl?: string;
7
+ headers?: Record<string, string>;
8
+ }
9
+ interface RepositorySummary {
10
+ provider: GitProvider;
11
+ id: string;
12
+ fullName: string;
13
+ name: string;
14
+ defaultBranch?: string;
15
+ webUrl?: string;
16
+ private?: boolean;
17
+ description?: string | null;
18
+ }
19
+ interface CommitSummary {
20
+ provider: GitProvider;
21
+ sha: string;
22
+ shortSha: string;
23
+ title: string;
24
+ message: string;
25
+ authorName?: string;
26
+ authorEmail?: string;
27
+ authoredAt?: string;
28
+ webUrl?: string;
29
+ }
30
+ interface CompareSummary {
31
+ provider: GitProvider;
32
+ fromSha: string;
33
+ toSha: string;
34
+ status?: string;
35
+ aheadBy?: number;
36
+ behindBy?: number;
37
+ totalCommits: number;
38
+ }
39
+ interface FileContentResult {
40
+ provider: GitProvider;
41
+ path: string;
42
+ ref: string;
43
+ sha?: string;
44
+ size?: number;
45
+ content: string;
46
+ }
47
+ interface ArchiveResult {
48
+ provider: GitProvider;
49
+ ref: string;
50
+ contentType?: string | null;
51
+ fileName?: string;
52
+ size: number;
53
+ data: Uint8Array;
54
+ }
55
+ declare class GitProviderClient {
56
+ private readonly provider;
57
+ private readonly token?;
58
+ private readonly baseUrl;
59
+ private readonly extraHeaders;
60
+ constructor(options: GitProviderClientOptions);
61
+ getRepositorySummary(idOrPath: string | number): Promise<RepositorySummary>;
62
+ getCommitSummary(repo: string | number, sha: string): Promise<CommitSummary>;
63
+ compareCommits(repo: string | number, fromSha: string, toSha: string): Promise<CompareSummary>;
64
+ getFileContent(repo: string | number, filePath: string, ref: string): Promise<FileContentResult>;
65
+ downloadArchive(repo: string | number, ref: string): Promise<ArchiveResult>;
66
+ private fetchGiteaProject;
67
+ private resolveOwnerRepo;
68
+ private parseOwnerRepo;
69
+ private encodeGitLabProjectId;
70
+ private encodePath;
71
+ private parseFileNameFromHeaders;
72
+ private requestJson;
73
+ private fetchWithAuth;
74
+ private getAuthHeaders;
75
+ }
76
+ declare function createGitProviderClient(options: GitProviderClientOptions): GitProviderClient;
77
+ //#endregion
78
+ export { ArchiveResult, CommitSummary, CompareSummary, FileContentResult, GitProvider, GitProviderClient, GitProviderClientOptions, RepositorySummary, createGitProviderClient };
package/dist/index.mjs ADDED
@@ -0,0 +1,289 @@
1
+ //#region src/index.ts
2
+ function stripTrailingSlash(value) {
3
+ return value.endsWith("/") ? value.slice(0, -1) : value;
4
+ }
5
+ function normalizeProviderBaseUrl(provider, baseUrl) {
6
+ if (baseUrl) return stripTrailingSlash(baseUrl);
7
+ if (provider === "github") return "https://api.github.com";
8
+ if (provider === "gitlab") return "https://gitlab.com/api/v4";
9
+ return "https://gitea.com/api/v1";
10
+ }
11
+ function shortSha(sha) {
12
+ return sha.slice(0, 8);
13
+ }
14
+ function decodeBase64ToUtf8(value) {
15
+ return Buffer.from(value.replace(/\n/g, ""), "base64").toString("utf-8");
16
+ }
17
+ function isLikelyNumericRepoId(repo) {
18
+ return typeof repo === "number" || /^\d+$/.test(repo);
19
+ }
20
+ var GitProviderClient = class {
21
+ provider;
22
+ token;
23
+ baseUrl;
24
+ extraHeaders;
25
+ constructor(options) {
26
+ this.provider = options.provider;
27
+ this.token = options.token;
28
+ this.baseUrl = normalizeProviderBaseUrl(options.provider, options.baseUrl);
29
+ this.extraHeaders = options.headers ?? {};
30
+ }
31
+ async getRepositorySummary(idOrPath) {
32
+ if (this.provider === "github") {
33
+ const url = isLikelyNumericRepoId(idOrPath) ? `${this.baseUrl}/repositories/${idOrPath}` : `${this.baseUrl}/repos/${encodeURIComponent(String(idOrPath).split("/")[0])}/${encodeURIComponent(String(idOrPath).split("/")[1] ?? "")}`;
34
+ const project = await this.requestJson(url);
35
+ return {
36
+ provider: this.provider,
37
+ id: String(project.id),
38
+ fullName: project.full_name,
39
+ name: project.name,
40
+ defaultBranch: project.default_branch,
41
+ webUrl: project.html_url,
42
+ private: project.private,
43
+ description: project.description
44
+ };
45
+ }
46
+ if (this.provider === "gitlab") {
47
+ const projectId = this.encodeGitLabProjectId(idOrPath);
48
+ const project = await this.requestJson(`${this.baseUrl}/projects/${projectId}`);
49
+ return {
50
+ provider: this.provider,
51
+ id: String(project.id),
52
+ fullName: project.path_with_namespace,
53
+ name: project.name,
54
+ defaultBranch: project.default_branch,
55
+ webUrl: project.web_url,
56
+ private: project.visibility ? project.visibility !== "public" : void 0,
57
+ description: project.description
58
+ };
59
+ }
60
+ const project = await this.fetchGiteaProject(idOrPath);
61
+ return {
62
+ provider: this.provider,
63
+ id: String(project.id),
64
+ fullName: project.full_name,
65
+ name: project.name,
66
+ defaultBranch: project.default_branch,
67
+ webUrl: project.html_url,
68
+ private: project.private,
69
+ description: project.description
70
+ };
71
+ }
72
+ async getCommitSummary(repo, sha) {
73
+ if (this.provider === "github") {
74
+ const ownerRepo = await this.resolveOwnerRepo(repo);
75
+ const commit = await this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/commits/${encodeURIComponent(sha)}`);
76
+ return {
77
+ provider: this.provider,
78
+ sha: commit.sha,
79
+ shortSha: shortSha(commit.sha),
80
+ title: commit.commit?.message?.split("\n")[0] ?? "",
81
+ message: commit.commit?.message ?? "",
82
+ authorName: commit.commit?.author?.name,
83
+ authorEmail: commit.commit?.author?.email,
84
+ authoredAt: commit.commit?.author?.date,
85
+ webUrl: commit.html_url
86
+ };
87
+ }
88
+ if (this.provider === "gitlab") {
89
+ const pid = this.encodeGitLabProjectId(repo);
90
+ const commit = await this.requestJson(`${this.baseUrl}/projects/${pid}/repository/commits/${encodeURIComponent(sha)}`);
91
+ return {
92
+ provider: this.provider,
93
+ sha: commit.id,
94
+ shortSha: shortSha(commit.short_id ?? commit.id),
95
+ title: commit.title ?? commit.message?.split("\n")[0] ?? "",
96
+ message: commit.message ?? "",
97
+ authorName: commit.author_name,
98
+ authorEmail: commit.author_email,
99
+ authoredAt: commit.authored_date,
100
+ webUrl: commit.web_url
101
+ };
102
+ }
103
+ const ownerRepo = await this.resolveOwnerRepo(repo);
104
+ const commit = await this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/commits/${encodeURIComponent(sha)}`);
105
+ return {
106
+ provider: this.provider,
107
+ sha: commit.sha,
108
+ shortSha: shortSha(commit.sha),
109
+ title: commit.commit?.message?.split("\n")[0] ?? "",
110
+ message: commit.commit?.message ?? "",
111
+ authorName: commit.commit?.author?.name,
112
+ authorEmail: commit.commit?.author?.email,
113
+ authoredAt: commit.commit?.author?.date,
114
+ webUrl: commit.html_url
115
+ };
116
+ }
117
+ async compareCommits(repo, fromSha, toSha) {
118
+ if (this.provider === "github") {
119
+ const ownerRepo = await this.resolveOwnerRepo(repo);
120
+ const compare = await this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/compare/${encodeURIComponent(fromSha)}...${encodeURIComponent(toSha)}`);
121
+ return {
122
+ provider: this.provider,
123
+ fromSha,
124
+ toSha,
125
+ status: compare.status,
126
+ aheadBy: compare.ahead_by,
127
+ behindBy: compare.behind_by,
128
+ totalCommits: compare.total_commits ?? compare.commits?.length ?? 0
129
+ };
130
+ }
131
+ if (this.provider === "gitlab") {
132
+ const pid = this.encodeGitLabProjectId(repo);
133
+ const compare = await this.requestJson(`${this.baseUrl}/projects/${pid}/repository/compare?from=${encodeURIComponent(fromSha)}&to=${encodeURIComponent(toSha)}`);
134
+ return {
135
+ provider: this.provider,
136
+ fromSha,
137
+ toSha,
138
+ status: compare.compare_same_ref ? "identical" : "different",
139
+ totalCommits: compare.commits?.length ?? 0
140
+ };
141
+ }
142
+ const ownerRepo = await this.resolveOwnerRepo(repo);
143
+ const compare = await this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/compare/${encodeURIComponent(fromSha)}...${encodeURIComponent(toSha)}`);
144
+ return {
145
+ provider: this.provider,
146
+ fromSha,
147
+ toSha,
148
+ status: compare.status,
149
+ aheadBy: compare.ahead_by,
150
+ behindBy: compare.behind_by,
151
+ totalCommits: compare.total_commits ?? compare.commits?.length ?? 0
152
+ };
153
+ }
154
+ async getFileContent(repo, filePath, ref) {
155
+ if (this.provider === "github") {
156
+ const ownerRepo = await this.resolveOwnerRepo(repo);
157
+ const payload = await this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/contents/${this.encodePath(filePath)}?ref=${encodeURIComponent(ref)}`);
158
+ return {
159
+ provider: this.provider,
160
+ path: payload.path ?? filePath,
161
+ ref,
162
+ sha: payload.sha,
163
+ size: payload.size,
164
+ content: decodeBase64ToUtf8(payload.content ?? "")
165
+ };
166
+ }
167
+ if (this.provider === "gitlab") {
168
+ const pid = this.encodeGitLabProjectId(repo);
169
+ const payload = await this.requestJson(`${this.baseUrl}/projects/${pid}/repository/files/${this.encodePath(filePath)}?ref=${encodeURIComponent(ref)}`);
170
+ return {
171
+ provider: this.provider,
172
+ path: payload.file_path ?? filePath,
173
+ ref,
174
+ sha: payload.blob_id,
175
+ size: payload.size,
176
+ content: decodeBase64ToUtf8(payload.content ?? "")
177
+ };
178
+ }
179
+ const ownerRepo = await this.resolveOwnerRepo(repo);
180
+ const payload = await this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/contents/${this.encodePath(filePath)}?ref=${encodeURIComponent(ref)}`);
181
+ return {
182
+ provider: this.provider,
183
+ path: payload.path ?? filePath,
184
+ ref,
185
+ sha: payload.sha,
186
+ size: payload.size,
187
+ content: decodeBase64ToUtf8(payload.content ?? "")
188
+ };
189
+ }
190
+ async downloadArchive(repo, ref) {
191
+ let url;
192
+ if (this.provider === "gitlab") {
193
+ const pid = this.encodeGitLabProjectId(repo);
194
+ url = `${this.baseUrl}/projects/${pid}/repository/archive.zip?sha=${encodeURIComponent(ref)}`;
195
+ } else {
196
+ const ownerRepo = await this.resolveOwnerRepo(repo);
197
+ if (this.provider === "github") url = `${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/zipball/${encodeURIComponent(ref)}`;
198
+ else url = `${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}/archive/${encodeURIComponent(ref)}.zip`;
199
+ }
200
+ const response = await this.fetchWithAuth(url, { headers: { Accept: "application/zip" } });
201
+ if (!response.ok) {
202
+ const body = await response.text();
203
+ throw new Error(`[${this.provider}] request failed (${response.status}): ${body}`);
204
+ }
205
+ const data = new Uint8Array(await response.arrayBuffer());
206
+ return {
207
+ provider: this.provider,
208
+ ref,
209
+ contentType: response.headers.get("content-type"),
210
+ fileName: this.parseFileNameFromHeaders(response.headers),
211
+ size: data.byteLength,
212
+ data
213
+ };
214
+ }
215
+ async fetchGiteaProject(idOrPath) {
216
+ if (!isLikelyNumericRepoId(idOrPath)) {
217
+ const ownerRepo = this.parseOwnerRepo(String(idOrPath));
218
+ return this.requestJson(`${this.baseUrl}/repos/${encodeURIComponent(ownerRepo.owner)}/${encodeURIComponent(ownerRepo.repo)}`);
219
+ }
220
+ try {
221
+ return await this.requestJson(`${this.baseUrl}/repositories/${idOrPath}`);
222
+ } catch {
223
+ throw new Error("[gitea] numeric repository id not found; try owner/repo form");
224
+ }
225
+ }
226
+ async resolveOwnerRepo(repo) {
227
+ if (!isLikelyNumericRepoId(repo)) return this.parseOwnerRepo(String(repo));
228
+ if (this.provider === "gitlab") {
229
+ const project = await this.getRepositorySummary(repo);
230
+ return this.parseOwnerRepo(project.fullName);
231
+ }
232
+ const project = await this.getRepositorySummary(repo);
233
+ return this.parseOwnerRepo(project.fullName);
234
+ }
235
+ parseOwnerRepo(raw) {
236
+ const parts = raw.split("/");
237
+ if (parts.length < 2 || !parts[0] || !parts[1]) throw new Error(`invalid repository identifier "${raw}", expected "owner/repo" or numeric id`);
238
+ return {
239
+ owner: parts[0],
240
+ repo: parts.slice(1).join("/")
241
+ };
242
+ }
243
+ encodeGitLabProjectId(idOrPath) {
244
+ if (isLikelyNumericRepoId(idOrPath)) return String(idOrPath);
245
+ return this.encodePath(String(idOrPath));
246
+ }
247
+ encodePath(path) {
248
+ return path.split("/").filter(Boolean).map((piece) => encodeURIComponent(piece)).join("%2F");
249
+ }
250
+ parseFileNameFromHeaders(headers) {
251
+ const disposition = headers.get("content-disposition");
252
+ if (!disposition) return;
253
+ const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
254
+ if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
255
+ return disposition.match(/filename="([^"]+)"/i)?.[1];
256
+ }
257
+ async requestJson(url) {
258
+ const response = await this.fetchWithAuth(url);
259
+ if (!response.ok) {
260
+ const body = await response.text();
261
+ throw new Error(`[${this.provider}] request failed (${response.status}): ${body}`);
262
+ }
263
+ return response.json();
264
+ }
265
+ async fetchWithAuth(url, init) {
266
+ const headers = new Headers(init?.headers);
267
+ for (const [key, value] of Object.entries(this.getAuthHeaders())) headers.set(key, value);
268
+ for (const [key, value] of Object.entries(this.extraHeaders)) headers.set(key, value);
269
+ return fetch(url, {
270
+ ...init,
271
+ headers
272
+ });
273
+ }
274
+ getAuthHeaders() {
275
+ const headers = {};
276
+ if (this.provider === "github") headers.Accept = "application/vnd.github+json";
277
+ else headers.Accept = "application/json";
278
+ if (!this.token) return headers;
279
+ if (this.provider === "gitlab") headers["PRIVATE-TOKEN"] = this.token;
280
+ else if (this.provider === "gitea") headers.Authorization = `token ${this.token}`;
281
+ else headers.Authorization = `Bearer ${this.token}`;
282
+ return headers;
283
+ }
284
+ };
285
+ function createGitProviderClient(options) {
286
+ return new GitProviderClient(options);
287
+ }
288
+ //#endregion
289
+ export { GitProviderClient, createGitProviderClient };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@canyonjs/git-provider",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "Unified Git provider client for GitHub, GitLab, and Gitea.",
6
+ "keywords": [
7
+ "git",
8
+ "github",
9
+ "gitlab",
10
+ "gitea",
11
+ "api"
12
+ ],
13
+ "author": "canyonjs",
14
+ "license": "MIT",
15
+ "homepage": "https://git.dev.sh.ctripcorp.com/canyon-project/git-provider#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://git.dev.sh.ctripcorp.com/canyon-project/git-provider.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://git.dev.sh.ctripcorp.com/canyon-project/git-provider/issues"
22
+ },
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": "./dist/index.mjs",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "sideEffects": false,
32
+ "devDependencies": {
33
+ "@types/node": "^25.5.0",
34
+ "@typescript/native-preview": "7.0.0-dev.20260328.1",
35
+ "bumpp": "^11.0.1",
36
+ "tsdown": "^0.21.7",
37
+ "typescript": "^6.0.2",
38
+ "vitest": "^4.1.2"
39
+ },
40
+ "scripts": {
41
+ "build": "tsdown",
42
+ "dev": "tsdown --watch",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit",
45
+ "release": "bumpp"
46
+ }
47
+ }