@spaceflow/core 0.1.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/CHANGELOG.md +1176 -0
- package/README.md +105 -0
- package/nest-cli.json +10 -0
- package/package.json +128 -0
- package/rspack.config.mjs +62 -0
- package/src/__mocks__/@opencode-ai/sdk.js +9 -0
- package/src/__mocks__/c12.ts +3 -0
- package/src/app.module.ts +18 -0
- package/src/config/ci.config.ts +29 -0
- package/src/config/config-loader.ts +101 -0
- package/src/config/config-reader.module.ts +16 -0
- package/src/config/config-reader.service.ts +133 -0
- package/src/config/feishu.config.ts +35 -0
- package/src/config/git-provider.config.ts +29 -0
- package/src/config/index.ts +29 -0
- package/src/config/llm.config.ts +110 -0
- package/src/config/schema-generator.service.ts +129 -0
- package/src/config/spaceflow.config.ts +292 -0
- package/src/config/storage.config.ts +33 -0
- package/src/extension-system/extension.interface.ts +221 -0
- package/src/extension-system/index.ts +1 -0
- package/src/index.ts +80 -0
- package/src/locales/en/translation.json +11 -0
- package/src/locales/zh-cn/translation.json +11 -0
- package/src/shared/claude-setup/claude-setup.module.ts +8 -0
- package/src/shared/claude-setup/claude-setup.service.ts +131 -0
- package/src/shared/claude-setup/index.ts +2 -0
- package/src/shared/editor-config/index.ts +23 -0
- package/src/shared/feishu-sdk/feishu-sdk.module.ts +77 -0
- package/src/shared/feishu-sdk/feishu-sdk.service.ts +130 -0
- package/src/shared/feishu-sdk/fieshu-card.service.ts +139 -0
- package/src/shared/feishu-sdk/index.ts +4 -0
- package/src/shared/feishu-sdk/types/card-action.ts +132 -0
- package/src/shared/feishu-sdk/types/card.ts +64 -0
- package/src/shared/feishu-sdk/types/common.ts +22 -0
- package/src/shared/feishu-sdk/types/index.ts +46 -0
- package/src/shared/feishu-sdk/types/message.ts +35 -0
- package/src/shared/feishu-sdk/types/module.ts +21 -0
- package/src/shared/feishu-sdk/types/user.ts +77 -0
- package/src/shared/git-provider/adapters/gitea.adapter.spec.ts +473 -0
- package/src/shared/git-provider/adapters/gitea.adapter.ts +499 -0
- package/src/shared/git-provider/adapters/github.adapter.spec.ts +341 -0
- package/src/shared/git-provider/adapters/github.adapter.ts +830 -0
- package/src/shared/git-provider/adapters/gitlab.adapter.ts +839 -0
- package/src/shared/git-provider/adapters/index.ts +3 -0
- package/src/shared/git-provider/detect-provider.spec.ts +195 -0
- package/src/shared/git-provider/detect-provider.ts +112 -0
- package/src/shared/git-provider/git-provider.interface.ts +188 -0
- package/src/shared/git-provider/git-provider.module.ts +73 -0
- package/src/shared/git-provider/git-provider.service.spec.ts +282 -0
- package/src/shared/git-provider/git-provider.service.ts +309 -0
- package/src/shared/git-provider/index.ts +7 -0
- package/src/shared/git-provider/parse-repo-url.spec.ts +221 -0
- package/src/shared/git-provider/parse-repo-url.ts +155 -0
- package/src/shared/git-provider/types.ts +434 -0
- package/src/shared/git-sdk/git-sdk-diff.utils.spec.ts +344 -0
- package/src/shared/git-sdk/git-sdk-diff.utils.ts +151 -0
- package/src/shared/git-sdk/git-sdk.module.ts +8 -0
- package/src/shared/git-sdk/git-sdk.service.ts +235 -0
- package/src/shared/git-sdk/git-sdk.types.ts +25 -0
- package/src/shared/git-sdk/index.ts +4 -0
- package/src/shared/i18n/i18n.spec.ts +96 -0
- package/src/shared/i18n/i18n.ts +86 -0
- package/src/shared/i18n/index.ts +1 -0
- package/src/shared/i18n/locale-detect.ts +134 -0
- package/src/shared/llm-jsonput/index.ts +94 -0
- package/src/shared/llm-jsonput/types.ts +17 -0
- package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +131 -0
- package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +208 -0
- package/src/shared/llm-proxy/adapters/index.ts +4 -0
- package/src/shared/llm-proxy/adapters/llm-adapter.interface.ts +23 -0
- package/src/shared/llm-proxy/adapters/open-code.adapter.ts +342 -0
- package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +215 -0
- package/src/shared/llm-proxy/adapters/openai.adapter.ts +153 -0
- package/src/shared/llm-proxy/index.ts +6 -0
- package/src/shared/llm-proxy/interfaces/config.interface.ts +32 -0
- package/src/shared/llm-proxy/interfaces/index.ts +4 -0
- package/src/shared/llm-proxy/interfaces/message.interface.ts +48 -0
- package/src/shared/llm-proxy/interfaces/session.interface.ts +28 -0
- package/src/shared/llm-proxy/llm-proxy.module.ts +140 -0
- package/src/shared/llm-proxy/llm-proxy.service.spec.ts +303 -0
- package/src/shared/llm-proxy/llm-proxy.service.ts +132 -0
- package/src/shared/llm-proxy/llm-session.spec.ts +111 -0
- package/src/shared/llm-proxy/llm-session.ts +109 -0
- package/src/shared/llm-proxy/stream-logger.ts +97 -0
- package/src/shared/logger/index.ts +11 -0
- package/src/shared/logger/logger.interface.ts +93 -0
- package/src/shared/logger/logger.spec.ts +178 -0
- package/src/shared/logger/logger.ts +175 -0
- package/src/shared/logger/renderers/plain.renderer.ts +116 -0
- package/src/shared/logger/renderers/tui.renderer.ts +162 -0
- package/src/shared/mcp/index.ts +332 -0
- package/src/shared/output/index.ts +2 -0
- package/src/shared/output/output.module.ts +9 -0
- package/src/shared/output/output.service.ts +97 -0
- package/src/shared/package-manager/index.ts +115 -0
- package/src/shared/parallel/index.ts +1 -0
- package/src/shared/parallel/parallel-executor.ts +169 -0
- package/src/shared/rspack-config/index.ts +1 -0
- package/src/shared/rspack-config/rspack-config.ts +157 -0
- package/src/shared/source-utils/index.ts +130 -0
- package/src/shared/spaceflow-dir/index.ts +158 -0
- package/src/shared/storage/adapters/file.adapter.ts +113 -0
- package/src/shared/storage/adapters/index.ts +3 -0
- package/src/shared/storage/adapters/memory.adapter.ts +50 -0
- package/src/shared/storage/adapters/storage-adapter.interface.ts +48 -0
- package/src/shared/storage/index.ts +4 -0
- package/src/shared/storage/storage.module.ts +150 -0
- package/src/shared/storage/storage.service.ts +293 -0
- package/src/shared/storage/types.ts +51 -0
- package/src/shared/verbose/index.ts +73 -0
- package/test/app.e2e-spec.ts +22 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +25 -0
- package/tsconfig.skill.json +18 -0
- package/vitest.config.ts +58 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import type {
|
|
3
|
+
GitProvider,
|
|
4
|
+
LockBranchOptions,
|
|
5
|
+
ListPullRequestsOptions,
|
|
6
|
+
} from "../git-provider.interface";
|
|
7
|
+
import type {
|
|
8
|
+
GitProviderModuleOptions,
|
|
9
|
+
BranchProtection,
|
|
10
|
+
CreateBranchProtectionOption,
|
|
11
|
+
EditBranchProtectionOption,
|
|
12
|
+
Branch,
|
|
13
|
+
Repository,
|
|
14
|
+
PullRequest,
|
|
15
|
+
PullRequestCommit,
|
|
16
|
+
ChangedFile,
|
|
17
|
+
CommitInfo,
|
|
18
|
+
IssueComment,
|
|
19
|
+
CreateIssueCommentOption,
|
|
20
|
+
CreateIssueOption,
|
|
21
|
+
Issue,
|
|
22
|
+
CreatePullReviewOption,
|
|
23
|
+
PullReview,
|
|
24
|
+
PullReviewComment,
|
|
25
|
+
Reaction,
|
|
26
|
+
EditPullRequestOption,
|
|
27
|
+
User,
|
|
28
|
+
RepositoryContent,
|
|
29
|
+
} from "../types";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* GitLab 平台适配器
|
|
33
|
+
*
|
|
34
|
+
* GitLab API 特点:
|
|
35
|
+
* - 使用 PRIVATE-TOKEN 认证
|
|
36
|
+
* - 项目通过 URL-encoded path(owner/repo)标识
|
|
37
|
+
* - Merge Request(MR)对应 Pull Request
|
|
38
|
+
* - Notes 对应 Comments/Reviews
|
|
39
|
+
* - API 前缀 /api/v4
|
|
40
|
+
*/
|
|
41
|
+
export class GitlabAdapter implements GitProvider {
|
|
42
|
+
protected readonly baseUrl: string;
|
|
43
|
+
protected readonly token: string;
|
|
44
|
+
|
|
45
|
+
constructor(protected readonly options: GitProviderModuleOptions) {
|
|
46
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
47
|
+
this.token = options.token;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 将 owner/repo 编码为 GitLab 项目路径 */
|
|
51
|
+
protected encodeProject(owner: string, repo: string): string {
|
|
52
|
+
return encodeURIComponent(`${owner}/${repo}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
validateConfig(): void {
|
|
56
|
+
if (!this.options?.baseUrl) {
|
|
57
|
+
throw new Error("缺少配置 gitProvider.baseUrl (环境变量 GIT_PROVIDER_URL)");
|
|
58
|
+
}
|
|
59
|
+
if (!this.options?.token) {
|
|
60
|
+
throw new Error("缺少配置 gitProvider.token (环境变量 GIT_PROVIDER_TOKEN)");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
65
|
+
const url = `${this.baseUrl}/api/v4${path}`;
|
|
66
|
+
const headers: Record<string, string> = {
|
|
67
|
+
"PRIVATE-TOKEN": this.token,
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
};
|
|
70
|
+
const response = await fetch(url, {
|
|
71
|
+
method,
|
|
72
|
+
headers,
|
|
73
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const errorText = await response.text();
|
|
77
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
78
|
+
}
|
|
79
|
+
if (response.status === 204) {
|
|
80
|
+
return {} as T;
|
|
81
|
+
}
|
|
82
|
+
return response.json() as Promise<T>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected async fetchText(path: string): Promise<string> {
|
|
86
|
+
const url = `${this.baseUrl}/api/v4${path}`;
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
headers: { "PRIVATE-TOKEN": this.token },
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const errorText = await response.text();
|
|
92
|
+
throw new Error(`GitLab API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
93
|
+
}
|
|
94
|
+
return response.text();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============ 仓库操作 ============
|
|
98
|
+
|
|
99
|
+
async getRepository(owner: string, repo: string): Promise<Repository> {
|
|
100
|
+
const project = this.encodeProject(owner, repo);
|
|
101
|
+
const result = await this.request<Record<string, unknown>>("GET", `/projects/${project}`);
|
|
102
|
+
const namespace = result.namespace as Record<string, unknown> | undefined;
|
|
103
|
+
return {
|
|
104
|
+
id: result.id as number,
|
|
105
|
+
name: result.name as string,
|
|
106
|
+
full_name: result.path_with_namespace as string,
|
|
107
|
+
default_branch: result.default_branch as string,
|
|
108
|
+
owner: namespace
|
|
109
|
+
? {
|
|
110
|
+
id: namespace.id as number,
|
|
111
|
+
login: namespace.path as string,
|
|
112
|
+
full_name: namespace.name as string,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============ 分支操作 ============
|
|
119
|
+
|
|
120
|
+
async getBranch(owner: string, repo: string, branch: string): Promise<Branch> {
|
|
121
|
+
const project = this.encodeProject(owner, repo);
|
|
122
|
+
const result = await this.request<Record<string, unknown>>(
|
|
123
|
+
"GET",
|
|
124
|
+
`/projects/${project}/repository/branches/${encodeURIComponent(branch)}`,
|
|
125
|
+
);
|
|
126
|
+
const commit = result.commit as Record<string, unknown> | undefined;
|
|
127
|
+
return {
|
|
128
|
+
name: result.name as string,
|
|
129
|
+
protected: result.protected as boolean,
|
|
130
|
+
commit: commit ? { id: commit.id as string, message: commit.message as string } : undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============ 分支保护 ============
|
|
135
|
+
|
|
136
|
+
async listBranchProtections(owner: string, repo: string): Promise<BranchProtection[]> {
|
|
137
|
+
const project = this.encodeProject(owner, repo);
|
|
138
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
139
|
+
"GET",
|
|
140
|
+
`/projects/${project}/protected_branches`,
|
|
141
|
+
);
|
|
142
|
+
return results.map((p) => this.mapProtection(p));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getBranchProtection(owner: string, repo: string, name: string): Promise<BranchProtection> {
|
|
146
|
+
const project = this.encodeProject(owner, repo);
|
|
147
|
+
const result = await this.request<Record<string, unknown>>(
|
|
148
|
+
"GET",
|
|
149
|
+
`/projects/${project}/protected_branches/${encodeURIComponent(name)}`,
|
|
150
|
+
);
|
|
151
|
+
return this.mapProtection(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async createBranchProtection(
|
|
155
|
+
owner: string,
|
|
156
|
+
repo: string,
|
|
157
|
+
options: CreateBranchProtectionOption,
|
|
158
|
+
): Promise<BranchProtection> {
|
|
159
|
+
const project = this.encodeProject(owner, repo);
|
|
160
|
+
const branchName = options.branch_name || options.rule_name || "";
|
|
161
|
+
const body: Record<string, unknown> = {
|
|
162
|
+
name: branchName,
|
|
163
|
+
push_access_level: options.enable_push ? 30 : 0, // 30=Developer, 0=No one
|
|
164
|
+
merge_access_level: 30,
|
|
165
|
+
};
|
|
166
|
+
const result = await this.request<Record<string, unknown>>(
|
|
167
|
+
"POST",
|
|
168
|
+
`/projects/${project}/protected_branches`,
|
|
169
|
+
body,
|
|
170
|
+
);
|
|
171
|
+
return this.mapProtection(result);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async editBranchProtection(
|
|
175
|
+
owner: string,
|
|
176
|
+
repo: string,
|
|
177
|
+
name: string,
|
|
178
|
+
options: EditBranchProtectionOption,
|
|
179
|
+
): Promise<BranchProtection> {
|
|
180
|
+
// GitLab 不支持直接编辑,需要先删除再创建
|
|
181
|
+
await this.deleteBranchProtection(owner, repo, name);
|
|
182
|
+
return this.createBranchProtection(owner, repo, {
|
|
183
|
+
branch_name: name,
|
|
184
|
+
rule_name: name,
|
|
185
|
+
...options,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async deleteBranchProtection(owner: string, repo: string, name: string): Promise<void> {
|
|
190
|
+
const project = this.encodeProject(owner, repo);
|
|
191
|
+
await this.request<void>(
|
|
192
|
+
"DELETE",
|
|
193
|
+
`/projects/${project}/protected_branches/${encodeURIComponent(name)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async lockBranch(
|
|
198
|
+
owner: string,
|
|
199
|
+
repo: string,
|
|
200
|
+
branch: string,
|
|
201
|
+
options?: LockBranchOptions,
|
|
202
|
+
): Promise<BranchProtection> {
|
|
203
|
+
// 先尝试删除已有保护
|
|
204
|
+
try {
|
|
205
|
+
await this.deleteBranchProtection(owner, repo, branch);
|
|
206
|
+
} catch {
|
|
207
|
+
// 不存在时忽略
|
|
208
|
+
}
|
|
209
|
+
const pushLevel = options?.pushWhitelistUsernames?.length ? 30 : 0;
|
|
210
|
+
return this.createBranchProtection(owner, repo, {
|
|
211
|
+
branch_name: branch,
|
|
212
|
+
rule_name: branch,
|
|
213
|
+
enable_push: pushLevel > 0,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async unlockBranch(
|
|
218
|
+
owner: string,
|
|
219
|
+
repo: string,
|
|
220
|
+
branch: string,
|
|
221
|
+
): Promise<BranchProtection | null> {
|
|
222
|
+
try {
|
|
223
|
+
const existing = await this.getBranchProtection(owner, repo, branch);
|
|
224
|
+
await this.deleteBranchProtection(owner, repo, branch);
|
|
225
|
+
return existing;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
unlockBranchSync(owner: string, repo: string, branch: string): void {
|
|
232
|
+
const project = this.encodeProject(owner, repo);
|
|
233
|
+
try {
|
|
234
|
+
execSync(
|
|
235
|
+
`curl -s -X DELETE "${this.baseUrl}/api/v4/projects/${project}/protected_branches/${encodeURIComponent(branch)}" -H "PRIVATE-TOKEN: ${this.token}"`,
|
|
236
|
+
{ encoding: "utf-8" },
|
|
237
|
+
);
|
|
238
|
+
console.log(`✅ 分支已解锁(同步): ${branch}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error("⚠️ 同步解锁分支失败:", error instanceof Error ? error.message : error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============ Merge Request(对应 Pull Request) ============
|
|
245
|
+
|
|
246
|
+
async getPullRequest(owner: string, repo: string, index: number): Promise<PullRequest> {
|
|
247
|
+
const project = this.encodeProject(owner, repo);
|
|
248
|
+
const result = await this.request<Record<string, unknown>>(
|
|
249
|
+
"GET",
|
|
250
|
+
`/projects/${project}/merge_requests/${index}`,
|
|
251
|
+
);
|
|
252
|
+
return this.mapMergeRequest(result);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async editPullRequest(
|
|
256
|
+
owner: string,
|
|
257
|
+
repo: string,
|
|
258
|
+
index: number,
|
|
259
|
+
options: EditPullRequestOption,
|
|
260
|
+
): Promise<PullRequest> {
|
|
261
|
+
const project = this.encodeProject(owner, repo);
|
|
262
|
+
const body: Record<string, unknown> = {};
|
|
263
|
+
if (options.title) body.title = options.title;
|
|
264
|
+
if (options.body !== undefined) body.description = options.body;
|
|
265
|
+
if (options.state) body.state_event = options.state === "closed" ? "close" : "reopen";
|
|
266
|
+
const result = await this.request<Record<string, unknown>>(
|
|
267
|
+
"PUT",
|
|
268
|
+
`/projects/${project}/merge_requests/${index}`,
|
|
269
|
+
body,
|
|
270
|
+
);
|
|
271
|
+
return this.mapMergeRequest(result);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async listPullRequests(
|
|
275
|
+
owner: string,
|
|
276
|
+
repo: string,
|
|
277
|
+
state?: "open" | "closed" | "all",
|
|
278
|
+
): Promise<PullRequest[]> {
|
|
279
|
+
const project = this.encodeProject(owner, repo);
|
|
280
|
+
const glState = this.mapStateParam(state);
|
|
281
|
+
const query = glState ? `?state=${glState}` : "";
|
|
282
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
283
|
+
"GET",
|
|
284
|
+
`/projects/${project}/merge_requests${query}`,
|
|
285
|
+
);
|
|
286
|
+
return results.map((mr) => this.mapMergeRequest(mr));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async listAllPullRequests(
|
|
290
|
+
owner: string,
|
|
291
|
+
repo: string,
|
|
292
|
+
options?: ListPullRequestsOptions,
|
|
293
|
+
): Promise<PullRequest[]> {
|
|
294
|
+
const project = this.encodeProject(owner, repo);
|
|
295
|
+
const allMRs: PullRequest[] = [];
|
|
296
|
+
let page = 1;
|
|
297
|
+
const perPage = 100;
|
|
298
|
+
while (true) {
|
|
299
|
+
const params = new URLSearchParams();
|
|
300
|
+
params.set("page", String(page));
|
|
301
|
+
params.set("per_page", String(perPage));
|
|
302
|
+
if (options?.state) params.set("state", this.mapStateParam(options.state) || "all");
|
|
303
|
+
if (options?.labels?.length) params.set("labels", options.labels.join(","));
|
|
304
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
305
|
+
"GET",
|
|
306
|
+
`/projects/${project}/merge_requests?${params.toString()}`,
|
|
307
|
+
);
|
|
308
|
+
if (!results || results.length === 0) break;
|
|
309
|
+
allMRs.push(...results.map((mr) => this.mapMergeRequest(mr)));
|
|
310
|
+
if (results.length < perPage) break;
|
|
311
|
+
page++;
|
|
312
|
+
}
|
|
313
|
+
return allMRs;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async getPullRequestCommits(
|
|
317
|
+
owner: string,
|
|
318
|
+
repo: string,
|
|
319
|
+
index: number,
|
|
320
|
+
): Promise<PullRequestCommit[]> {
|
|
321
|
+
const project = this.encodeProject(owner, repo);
|
|
322
|
+
const allCommits: PullRequestCommit[] = [];
|
|
323
|
+
let page = 1;
|
|
324
|
+
const perPage = 100;
|
|
325
|
+
while (true) {
|
|
326
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
327
|
+
"GET",
|
|
328
|
+
`/projects/${project}/merge_requests/${index}/commits?page=${page}&per_page=${perPage}`,
|
|
329
|
+
);
|
|
330
|
+
if (!results || results.length === 0) break;
|
|
331
|
+
allCommits.push(...results.map((c) => this.mapGitlabCommit(c)));
|
|
332
|
+
if (results.length < perPage) break;
|
|
333
|
+
page++;
|
|
334
|
+
}
|
|
335
|
+
return allCommits;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async getPullRequestFiles(owner: string, repo: string, index: number): Promise<ChangedFile[]> {
|
|
339
|
+
const project = this.encodeProject(owner, repo);
|
|
340
|
+
const allFiles: ChangedFile[] = [];
|
|
341
|
+
let page = 1;
|
|
342
|
+
const perPage = 100;
|
|
343
|
+
while (true) {
|
|
344
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
345
|
+
"GET",
|
|
346
|
+
`/projects/${project}/merge_requests/${index}/diffs?page=${page}&per_page=${perPage}`,
|
|
347
|
+
);
|
|
348
|
+
if (!results || results.length === 0) break;
|
|
349
|
+
allFiles.push(...results.map((d) => this.mapDiffToChangedFile(d)));
|
|
350
|
+
if (results.length < perPage) break;
|
|
351
|
+
page++;
|
|
352
|
+
}
|
|
353
|
+
return allFiles;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async getPullRequestDiff(owner: string, repo: string, index: number): Promise<string> {
|
|
357
|
+
const project = this.encodeProject(owner, repo);
|
|
358
|
+
return this.fetchText(`/projects/${project}/merge_requests/${index}/raw_diffs`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============ Commit 操作 ============
|
|
362
|
+
|
|
363
|
+
async getCommit(owner: string, repo: string, sha: string): Promise<CommitInfo> {
|
|
364
|
+
const project = this.encodeProject(owner, repo);
|
|
365
|
+
const result = await this.request<Record<string, unknown>>(
|
|
366
|
+
"GET",
|
|
367
|
+
`/projects/${project}/repository/commits/${sha}`,
|
|
368
|
+
);
|
|
369
|
+
// 获取 commit 的 diff 来填充 files
|
|
370
|
+
let files: ChangedFile[] = [];
|
|
371
|
+
try {
|
|
372
|
+
const diffs = await this.request<Array<Record<string, unknown>>>(
|
|
373
|
+
"GET",
|
|
374
|
+
`/projects/${project}/repository/commits/${sha}/diff`,
|
|
375
|
+
);
|
|
376
|
+
files = diffs.map((d) => this.mapDiffToChangedFile(d));
|
|
377
|
+
} catch {
|
|
378
|
+
// diff 获取失败时忽略
|
|
379
|
+
}
|
|
380
|
+
const commit = this.mapGitlabCommit(result);
|
|
381
|
+
return { ...commit, files };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async getCompareDiff(
|
|
385
|
+
owner: string,
|
|
386
|
+
repo: string,
|
|
387
|
+
baseSha: string,
|
|
388
|
+
headSha: string,
|
|
389
|
+
): Promise<string> {
|
|
390
|
+
const project = this.encodeProject(owner, repo);
|
|
391
|
+
const result = await this.request<{ diffs: Array<Record<string, unknown>> }>(
|
|
392
|
+
"GET",
|
|
393
|
+
`/projects/${project}/repository/compare?from=${encodeURIComponent(baseSha)}&to=${encodeURIComponent(headSha)}`,
|
|
394
|
+
);
|
|
395
|
+
// 将 diffs 拼接为 unified diff 文本
|
|
396
|
+
return (result.diffs || [])
|
|
397
|
+
.map((d) => {
|
|
398
|
+
const oldPath = d.old_path as string;
|
|
399
|
+
const newPath = d.new_path as string;
|
|
400
|
+
const diff = d.diff as string;
|
|
401
|
+
return `diff --git a/${oldPath} b/${newPath}\n${diff}`;
|
|
402
|
+
})
|
|
403
|
+
.join("\n");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async getCommitDiff(owner: string, repo: string, sha: string): Promise<string> {
|
|
407
|
+
const project = this.encodeProject(owner, repo);
|
|
408
|
+
const diffs = await this.request<Array<Record<string, unknown>>>(
|
|
409
|
+
"GET",
|
|
410
|
+
`/projects/${project}/repository/commits/${sha}/diff`,
|
|
411
|
+
);
|
|
412
|
+
return diffs
|
|
413
|
+
.map((d) => {
|
|
414
|
+
const oldPath = d.old_path as string;
|
|
415
|
+
const newPath = d.new_path as string;
|
|
416
|
+
const diff = d.diff as string;
|
|
417
|
+
return `diff --git a/${oldPath} b/${newPath}\n${diff}`;
|
|
418
|
+
})
|
|
419
|
+
.join("\n");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ============ 文件操作 ============
|
|
423
|
+
|
|
424
|
+
async getFileContent(
|
|
425
|
+
owner: string,
|
|
426
|
+
repo: string,
|
|
427
|
+
filepath: string,
|
|
428
|
+
ref?: string,
|
|
429
|
+
): Promise<string> {
|
|
430
|
+
const project = this.encodeProject(owner, repo);
|
|
431
|
+
const encodedPath = encodeURIComponent(filepath);
|
|
432
|
+
const query = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
|
433
|
+
try {
|
|
434
|
+
const url = `${this.baseUrl}/api/v4/projects/${project}/repository/files/${encodedPath}/raw${query}`;
|
|
435
|
+
const response = await fetch(url, {
|
|
436
|
+
headers: { "PRIVATE-TOKEN": this.token },
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
if (response.status === 404) {
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
const errorText = await response.text();
|
|
443
|
+
throw new Error(
|
|
444
|
+
`GitLab API error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return response.text();
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
450
|
+
return "";
|
|
451
|
+
}
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async listRepositoryContents(
|
|
457
|
+
owner: string,
|
|
458
|
+
repo: string,
|
|
459
|
+
path = "",
|
|
460
|
+
ref?: string,
|
|
461
|
+
): Promise<RepositoryContent[]> {
|
|
462
|
+
const project = this.encodeProject(owner, repo);
|
|
463
|
+
const params = new URLSearchParams();
|
|
464
|
+
if (path) params.set("path", path);
|
|
465
|
+
if (ref) params.set("ref", ref);
|
|
466
|
+
const result = await this.request<Array<Record<string, unknown>>>(
|
|
467
|
+
"GET",
|
|
468
|
+
`/projects/${project}/repository/tree?${params.toString()}`,
|
|
469
|
+
);
|
|
470
|
+
return result.map((item) => ({
|
|
471
|
+
name: item.name as string,
|
|
472
|
+
path: item.path as string,
|
|
473
|
+
type: (item.type as string) === "tree" ? ("dir" as const) : ("file" as const),
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ============ Issue 操作 ============
|
|
478
|
+
|
|
479
|
+
async createIssue(owner: string, repo: string, options: CreateIssueOption): Promise<Issue> {
|
|
480
|
+
const project = this.encodeProject(owner, repo);
|
|
481
|
+
const body: Record<string, unknown> = {
|
|
482
|
+
title: options.title,
|
|
483
|
+
description: options.body,
|
|
484
|
+
assignee_ids: options.assignees,
|
|
485
|
+
labels: options.labels?.join(","),
|
|
486
|
+
milestone_id: options.milestone,
|
|
487
|
+
};
|
|
488
|
+
const result = await this.request<Record<string, unknown>>(
|
|
489
|
+
"POST",
|
|
490
|
+
`/projects/${project}/issues`,
|
|
491
|
+
body,
|
|
492
|
+
);
|
|
493
|
+
return this.mapIssue(result);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async listIssueComments(owner: string, repo: string, index: number): Promise<IssueComment[]> {
|
|
497
|
+
const project = this.encodeProject(owner, repo);
|
|
498
|
+
// GitLab: MR notes 作为 issue comments
|
|
499
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
500
|
+
"GET",
|
|
501
|
+
`/projects/${project}/merge_requests/${index}/notes?sort=asc`,
|
|
502
|
+
);
|
|
503
|
+
return results.filter((n) => !(n.system as boolean)).map((n) => this.mapNote(n));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async createIssueComment(
|
|
507
|
+
owner: string,
|
|
508
|
+
repo: string,
|
|
509
|
+
index: number,
|
|
510
|
+
options: CreateIssueCommentOption,
|
|
511
|
+
): Promise<IssueComment> {
|
|
512
|
+
const project = this.encodeProject(owner, repo);
|
|
513
|
+
const result = await this.request<Record<string, unknown>>(
|
|
514
|
+
"POST",
|
|
515
|
+
`/projects/${project}/merge_requests/${index}/notes`,
|
|
516
|
+
{ body: options.body },
|
|
517
|
+
);
|
|
518
|
+
return this.mapNote(result);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async updateIssueComment(
|
|
522
|
+
_owner: string,
|
|
523
|
+
_repo: string,
|
|
524
|
+
_commentId: number,
|
|
525
|
+
_body: string,
|
|
526
|
+
): Promise<IssueComment> {
|
|
527
|
+
// GitLab 更新 note 需要 noteable_iid,接口签名不含此信息
|
|
528
|
+
throw new Error("GitLab 适配器暂不支持通过 commentId 更新评论,请使用 createIssueComment 替代");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async deleteIssueComment(_owner: string, _repo: string, _commentId: number): Promise<void> {
|
|
532
|
+
throw new Error("GitLab 适配器暂不支持通过 commentId 删除评论");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ============ MR Review(对应 PR Review) ============
|
|
536
|
+
|
|
537
|
+
async createPullReview(
|
|
538
|
+
owner: string,
|
|
539
|
+
repo: string,
|
|
540
|
+
index: number,
|
|
541
|
+
options: CreatePullReviewOption,
|
|
542
|
+
): Promise<PullReview> {
|
|
543
|
+
const project = this.encodeProject(owner, repo);
|
|
544
|
+
// GitLab 没有 review 概念,用 note 模拟
|
|
545
|
+
// 如果有 body,创建一个总体评论
|
|
546
|
+
if (options.body) {
|
|
547
|
+
const result = await this.request<Record<string, unknown>>(
|
|
548
|
+
"POST",
|
|
549
|
+
`/projects/${project}/merge_requests/${index}/notes`,
|
|
550
|
+
{ body: options.body },
|
|
551
|
+
);
|
|
552
|
+
const note = this.mapNote(result);
|
|
553
|
+
return {
|
|
554
|
+
id: note.id,
|
|
555
|
+
body: note.body,
|
|
556
|
+
state: options.event || "COMMENT",
|
|
557
|
+
user: note.user,
|
|
558
|
+
created_at: note.created_at,
|
|
559
|
+
updated_at: note.updated_at,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
// 如果有行级评论,逐个创建
|
|
563
|
+
if (options.comments?.length) {
|
|
564
|
+
for (const comment of options.comments) {
|
|
565
|
+
await this.request<Record<string, unknown>>(
|
|
566
|
+
"POST",
|
|
567
|
+
`/projects/${project}/merge_requests/${index}/notes`,
|
|
568
|
+
{ body: `**${comment.path}** (line ${comment.new_position})\n\n${comment.body}` },
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// 如果是 APPROVE 事件,调用 approve API
|
|
573
|
+
if (options.event === "APPROVE") {
|
|
574
|
+
await this.request<void>("POST", `/projects/${project}/merge_requests/${index}/approve`);
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
id: 0,
|
|
578
|
+
body: options.body || "",
|
|
579
|
+
state: options.event || "COMMENT",
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async listPullReviews(owner: string, repo: string, index: number): Promise<PullReview[]> {
|
|
584
|
+
const project = this.encodeProject(owner, repo);
|
|
585
|
+
// GitLab 没有 review 概念,用 notes 模拟
|
|
586
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
587
|
+
"GET",
|
|
588
|
+
`/projects/${project}/merge_requests/${index}/notes?sort=asc`,
|
|
589
|
+
);
|
|
590
|
+
return results
|
|
591
|
+
.filter((n) => !(n.system as boolean))
|
|
592
|
+
.map((n) => {
|
|
593
|
+
const note = this.mapNote(n);
|
|
594
|
+
return {
|
|
595
|
+
id: note.id,
|
|
596
|
+
body: note.body,
|
|
597
|
+
state: "COMMENT",
|
|
598
|
+
user: note.user,
|
|
599
|
+
created_at: note.created_at,
|
|
600
|
+
updated_at: note.updated_at,
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async deletePullReview(
|
|
606
|
+
owner: string,
|
|
607
|
+
repo: string,
|
|
608
|
+
index: number,
|
|
609
|
+
reviewId: number,
|
|
610
|
+
): Promise<void> {
|
|
611
|
+
const project = this.encodeProject(owner, repo);
|
|
612
|
+
await this.request<void>(
|
|
613
|
+
"DELETE",
|
|
614
|
+
`/projects/${project}/merge_requests/${index}/notes/${reviewId}`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async listPullReviewComments(
|
|
619
|
+
owner: string,
|
|
620
|
+
repo: string,
|
|
621
|
+
index: number,
|
|
622
|
+
_reviewId: number,
|
|
623
|
+
): Promise<PullReviewComment[]> {
|
|
624
|
+
// GitLab 没有 review 下的 comments 概念,返回所有 diff notes
|
|
625
|
+
const project = this.encodeProject(owner, repo);
|
|
626
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
627
|
+
"GET",
|
|
628
|
+
`/projects/${project}/merge_requests/${index}/notes?sort=asc`,
|
|
629
|
+
);
|
|
630
|
+
return results
|
|
631
|
+
.filter((n) => !!(n.position as Record<string, unknown> | undefined))
|
|
632
|
+
.map((n) => {
|
|
633
|
+
const user = (n.author as Record<string, unknown>) || {};
|
|
634
|
+
const position = (n.position as Record<string, unknown>) || {};
|
|
635
|
+
return {
|
|
636
|
+
id: n.id as number,
|
|
637
|
+
body: n.body as string,
|
|
638
|
+
path: (position.new_path || position.old_path) as string,
|
|
639
|
+
position: position.new_line as number,
|
|
640
|
+
original_position: position.old_line as number,
|
|
641
|
+
user: { id: user.id as number, login: user.username as string },
|
|
642
|
+
created_at: n.created_at as string,
|
|
643
|
+
updated_at: n.updated_at as string,
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ============ Reaction 操作 ============
|
|
649
|
+
|
|
650
|
+
async getIssueCommentReactions(
|
|
651
|
+
_owner: string,
|
|
652
|
+
_repo: string,
|
|
653
|
+
_commentId: number,
|
|
654
|
+
): Promise<Reaction[]> {
|
|
655
|
+
// GitLab: award emoji on notes(需要 noteable_iid,此处简化返回空)
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async getIssueReactions(owner: string, repo: string, index: number): Promise<Reaction[]> {
|
|
660
|
+
const project = this.encodeProject(owner, repo);
|
|
661
|
+
try {
|
|
662
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
663
|
+
"GET",
|
|
664
|
+
`/projects/${project}/merge_requests/${index}/award_emoji`,
|
|
665
|
+
);
|
|
666
|
+
return results.map((r) => {
|
|
667
|
+
const user = r.user as Record<string, unknown> | undefined;
|
|
668
|
+
return {
|
|
669
|
+
user: user ? { id: user.id as number, login: user.username as string } : undefined,
|
|
670
|
+
content: r.name as string,
|
|
671
|
+
created_at: r.created_at as string,
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
} catch {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ============ 用户操作 ============
|
|
680
|
+
|
|
681
|
+
async searchUsers(query: string, limit = 10): Promise<User[]> {
|
|
682
|
+
const params = new URLSearchParams();
|
|
683
|
+
params.set("search", query);
|
|
684
|
+
params.set("per_page", String(limit));
|
|
685
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
686
|
+
"GET",
|
|
687
|
+
`/users?${params.toString()}`,
|
|
688
|
+
);
|
|
689
|
+
return results.map((u) => ({
|
|
690
|
+
id: u.id as number,
|
|
691
|
+
login: u.username as string,
|
|
692
|
+
full_name: u.name as string,
|
|
693
|
+
email: u.email as string,
|
|
694
|
+
avatar_url: u.avatar_url as string,
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async getTeamMembers(teamId: number): Promise<User[]> {
|
|
699
|
+
// GitLab: group members
|
|
700
|
+
const results = await this.request<Array<Record<string, unknown>>>(
|
|
701
|
+
"GET",
|
|
702
|
+
`/groups/${teamId}/members`,
|
|
703
|
+
);
|
|
704
|
+
return results.map((u) => ({
|
|
705
|
+
id: u.id as number,
|
|
706
|
+
login: u.username as string,
|
|
707
|
+
full_name: u.name as string,
|
|
708
|
+
avatar_url: u.avatar_url as string,
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ============ 映射辅助方法 ============
|
|
713
|
+
|
|
714
|
+
protected mapProtection(data: Record<string, unknown>): BranchProtection {
|
|
715
|
+
const pushAccess = data.push_access_levels as Array<Record<string, unknown>> | undefined;
|
|
716
|
+
return {
|
|
717
|
+
branch_name: data.name as string,
|
|
718
|
+
rule_name: data.name as string,
|
|
719
|
+
enable_push: pushAccess ? pushAccess.some((l) => (l.access_level as number) > 0) : false,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
protected mapMergeRequest(data: Record<string, unknown>): PullRequest {
|
|
724
|
+
const author = data.author as Record<string, unknown> | undefined;
|
|
725
|
+
const reviewers = data.reviewers as Array<Record<string, unknown>> | undefined;
|
|
726
|
+
return {
|
|
727
|
+
id: data.id as number,
|
|
728
|
+
number: data.iid as number,
|
|
729
|
+
title: data.title as string,
|
|
730
|
+
body: data.description as string,
|
|
731
|
+
state: data.state as string,
|
|
732
|
+
head: {
|
|
733
|
+
ref: data.source_branch as string,
|
|
734
|
+
sha: data.sha as string,
|
|
735
|
+
},
|
|
736
|
+
base: {
|
|
737
|
+
ref: data.target_branch as string,
|
|
738
|
+
sha: data.diff_refs
|
|
739
|
+
? ((data.diff_refs as Record<string, unknown>).base_sha as string)
|
|
740
|
+
: undefined,
|
|
741
|
+
},
|
|
742
|
+
user: author ? { id: author.id as number, login: author.username as string } : undefined,
|
|
743
|
+
requested_reviewers: reviewers?.map((r) => ({
|
|
744
|
+
id: r.id as number,
|
|
745
|
+
login: r.username as string,
|
|
746
|
+
})),
|
|
747
|
+
created_at: data.created_at as string,
|
|
748
|
+
updated_at: data.updated_at as string,
|
|
749
|
+
merged_at: data.merged_at as string,
|
|
750
|
+
merge_base: data.merge_commit_sha as string,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
protected mapGitlabCommit(data: Record<string, unknown>): PullRequestCommit {
|
|
755
|
+
return {
|
|
756
|
+
sha: (data.id || data.sha) as string,
|
|
757
|
+
commit: {
|
|
758
|
+
message: (data.message || data.title) as string,
|
|
759
|
+
author: {
|
|
760
|
+
name: data.author_name as string,
|
|
761
|
+
email: data.author_email as string,
|
|
762
|
+
date: (data.authored_date || data.created_at) as string,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
author: data.author_name ? { login: data.author_name as string } : undefined,
|
|
766
|
+
committer: data.committer_name ? { login: data.committer_name as string } : undefined,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
protected mapDiffToChangedFile(data: Record<string, unknown>): ChangedFile {
|
|
771
|
+
let status = "modified";
|
|
772
|
+
if (data.new_file) status = "added";
|
|
773
|
+
else if (data.deleted_file) status = "deleted";
|
|
774
|
+
else if (data.renamed_file) status = "renamed";
|
|
775
|
+
const diff = data.diff as string | undefined;
|
|
776
|
+
// 从 diff 中计算 additions/deletions
|
|
777
|
+
let additions = 0;
|
|
778
|
+
let deletions = 0;
|
|
779
|
+
if (diff) {
|
|
780
|
+
for (const line of diff.split("\n")) {
|
|
781
|
+
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
782
|
+
if (line.startsWith("-") && !line.startsWith("---")) deletions++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
filename: (data.new_path || data.old_path) as string,
|
|
787
|
+
status,
|
|
788
|
+
additions,
|
|
789
|
+
deletions,
|
|
790
|
+
changes: additions + deletions,
|
|
791
|
+
patch: diff,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
protected mapNote(data: Record<string, unknown>): IssueComment {
|
|
796
|
+
const author = data.author as Record<string, unknown> | undefined;
|
|
797
|
+
return {
|
|
798
|
+
id: data.id as number,
|
|
799
|
+
body: data.body as string,
|
|
800
|
+
user: author ? { id: author.id as number, login: author.username as string } : undefined,
|
|
801
|
+
created_at: data.created_at as string,
|
|
802
|
+
updated_at: data.updated_at as string,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
protected mapIssue(data: Record<string, unknown>): Issue {
|
|
807
|
+
const author = data.author as Record<string, unknown> | undefined;
|
|
808
|
+
const labels = data.labels as string[] | undefined;
|
|
809
|
+
const assignees = data.assignees as Array<Record<string, unknown>> | undefined;
|
|
810
|
+
const milestone = data.milestone as Record<string, unknown> | undefined;
|
|
811
|
+
return {
|
|
812
|
+
id: data.id as number,
|
|
813
|
+
number: data.iid as number,
|
|
814
|
+
title: data.title as string,
|
|
815
|
+
body: data.description as string,
|
|
816
|
+
state: data.state as string,
|
|
817
|
+
user: author ? { id: author.id as number, login: author.username as string } : undefined,
|
|
818
|
+
labels: labels?.map((l) => ({ name: l })),
|
|
819
|
+
assignees: assignees?.map((a) => ({ id: a.id as number, login: a.username as string })),
|
|
820
|
+
milestone: milestone
|
|
821
|
+
? { id: milestone.id as number, title: milestone.title as string }
|
|
822
|
+
: undefined,
|
|
823
|
+
created_at: data.created_at as string,
|
|
824
|
+
updated_at: data.updated_at as string,
|
|
825
|
+
closed_at: data.closed_at as string,
|
|
826
|
+
html_url: data.web_url as string,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
protected mapStateParam(state?: "open" | "closed" | "all"): string | undefined {
|
|
831
|
+
if (!state) return undefined;
|
|
832
|
+
const stateMap: Record<string, string> = {
|
|
833
|
+
open: "opened",
|
|
834
|
+
closed: "closed",
|
|
835
|
+
all: "all",
|
|
836
|
+
};
|
|
837
|
+
return stateMap[state] || state;
|
|
838
|
+
}
|
|
839
|
+
}
|