@forge-glance/sdk 0.1.0 → 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/dist/GitHubProvider.d.ts +6 -1
- package/dist/GitHubProvider.js +123 -0
- package/dist/GitLabProvider.d.ts +6 -1
- package/dist/GitLabProvider.js +109 -0
- package/dist/GitProvider.d.ts +26 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +232 -0
- package/dist/providers.js +232 -0
- package/dist/types.d.ts +48 -0
- package/package.json +1 -1
- package/src/GitHubProvider.ts +149 -0
- package/src/GitLabProvider.ts +125 -0
- package/src/GitProvider.ts +51 -1
- package/src/index.ts +3 -0
- package/src/types.ts +53 -0
package/dist/GitHubProvider.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* provides the instance URL and we append "/api/v3".
|
|
13
13
|
*/
|
|
14
14
|
import type { GitProvider } from "./GitProvider.ts";
|
|
15
|
-
import type { MRDetail, PullRequest, UserRef } from "./types.ts";
|
|
15
|
+
import type { BranchProtectionRule, CreatePullRequestInput, MRDetail, PullRequest, UpdatePullRequestInput, UserRef } from "./types.ts";
|
|
16
16
|
import { type ForgeLogger } from "./logger.ts";
|
|
17
17
|
export declare class GitHubProvider implements GitProvider {
|
|
18
18
|
readonly providerName: "github";
|
|
@@ -33,6 +33,11 @@ export declare class GitHubProvider implements GitProvider {
|
|
|
33
33
|
fetchPullRequests(): Promise<PullRequest[]>;
|
|
34
34
|
fetchSingleMR(projectPath: string, mrIid: number, _currentUserNumericId: number | null): Promise<PullRequest | null>;
|
|
35
35
|
fetchMRDiscussions(repositoryId: string, mrIid: number): Promise<MRDetail>;
|
|
36
|
+
fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]>;
|
|
37
|
+
deleteBranch(projectPath: string, branch: string): Promise<void>;
|
|
38
|
+
fetchPullRequestByBranch(projectPath: string, sourceBranch: string): Promise<PullRequest | null>;
|
|
39
|
+
createPullRequest(input: CreatePullRequestInput): Promise<PullRequest>;
|
|
40
|
+
updatePullRequest(projectPath: string, mrIid: number, input: UpdatePullRequestInput): Promise<PullRequest>;
|
|
36
41
|
restRequest(method: string, path: string, body?: unknown): Promise<Response>;
|
|
37
42
|
private api;
|
|
38
43
|
/**
|
package/dist/GitHubProvider.js
CHANGED
|
@@ -207,6 +207,129 @@ class GitHubProvider {
|
|
|
207
207
|
}
|
|
208
208
|
return { mrIid, repositoryId, discussions };
|
|
209
209
|
}
|
|
210
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
211
|
+
const res = await this.api("GET", `/repos/${projectPath}/branches?protected=true&per_page=100`);
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
214
|
+
}
|
|
215
|
+
const branches = await res.json();
|
|
216
|
+
const rules = [];
|
|
217
|
+
for (const b of branches) {
|
|
218
|
+
if (!b.protected)
|
|
219
|
+
continue;
|
|
220
|
+
const detailRes = await this.api("GET", `/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`);
|
|
221
|
+
if (!detailRes.ok) {
|
|
222
|
+
rules.push({
|
|
223
|
+
pattern: b.name,
|
|
224
|
+
allowForcePush: false,
|
|
225
|
+
allowDeletion: false,
|
|
226
|
+
requiredApprovals: 0,
|
|
227
|
+
requireStatusChecks: false
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const detail = await detailRes.json();
|
|
232
|
+
rules.push({
|
|
233
|
+
pattern: b.name,
|
|
234
|
+
allowForcePush: detail.allow_force_pushes?.enabled ?? false,
|
|
235
|
+
allowDeletion: detail.allow_deletions?.enabled ?? false,
|
|
236
|
+
requiredApprovals: detail.required_pull_request_reviews?.required_approving_review_count ?? 0,
|
|
237
|
+
requireStatusChecks: detail.required_status_checks !== null && detail.required_status_checks !== undefined,
|
|
238
|
+
raw: detail
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return rules;
|
|
242
|
+
}
|
|
243
|
+
async deleteBranch(projectPath, branch) {
|
|
244
|
+
const res = await this.api("DELETE", `/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`);
|
|
245
|
+
if (!res.ok) {
|
|
246
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
250
|
+
const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const prs = await res.json();
|
|
256
|
+
if (!prs[0])
|
|
257
|
+
return null;
|
|
258
|
+
return this.fetchSingleMR(projectPath, prs[0].number, null);
|
|
259
|
+
}
|
|
260
|
+
async createPullRequest(input) {
|
|
261
|
+
const body = {
|
|
262
|
+
head: input.sourceBranch,
|
|
263
|
+
base: input.targetBranch,
|
|
264
|
+
title: input.title
|
|
265
|
+
};
|
|
266
|
+
if (input.description != null)
|
|
267
|
+
body.body = input.description;
|
|
268
|
+
if (input.draft != null)
|
|
269
|
+
body.draft = input.draft;
|
|
270
|
+
const res = await this.api("POST", `/repos/${input.projectPath}/pulls`, body);
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
const text = await res.text();
|
|
273
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
274
|
+
}
|
|
275
|
+
const created = await res.json();
|
|
276
|
+
if (input.reviewers?.length) {
|
|
277
|
+
await this.api("POST", `/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`, {
|
|
278
|
+
reviewers: input.reviewers
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (input.assignees?.length) {
|
|
282
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/assignees`, {
|
|
283
|
+
assignees: input.assignees
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (input.labels?.length) {
|
|
287
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/labels`, {
|
|
288
|
+
labels: input.labels
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.number, null);
|
|
292
|
+
if (!pr)
|
|
293
|
+
throw new Error("Created PR but failed to fetch it back");
|
|
294
|
+
return pr;
|
|
295
|
+
}
|
|
296
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
297
|
+
const body = {};
|
|
298
|
+
if (input.title != null)
|
|
299
|
+
body.title = input.title;
|
|
300
|
+
if (input.description != null)
|
|
301
|
+
body.body = input.description;
|
|
302
|
+
if (input.draft != null)
|
|
303
|
+
body.draft = input.draft;
|
|
304
|
+
if (input.targetBranch != null)
|
|
305
|
+
body.base = input.targetBranch;
|
|
306
|
+
if (input.stateEvent)
|
|
307
|
+
body.state = input.stateEvent === "close" ? "closed" : "open";
|
|
308
|
+
const res = await this.api("PATCH", `/repos/${projectPath}/pulls/${mrIid}`, body);
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
const text = await res.text();
|
|
311
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
312
|
+
}
|
|
313
|
+
if (input.reviewers) {
|
|
314
|
+
await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, {
|
|
315
|
+
reviewers: input.reviewers
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (input.assignees) {
|
|
319
|
+
await this.api("POST", `/repos/${projectPath}/issues/${mrIid}/assignees`, {
|
|
320
|
+
assignees: input.assignees
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (input.labels) {
|
|
324
|
+
await this.api("PUT", `/repos/${projectPath}/issues/${mrIid}/labels`, {
|
|
325
|
+
labels: input.labels
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
329
|
+
if (!pr)
|
|
330
|
+
throw new Error("Updated PR but failed to fetch it back");
|
|
331
|
+
return pr;
|
|
332
|
+
}
|
|
210
333
|
async restRequest(method, path, body) {
|
|
211
334
|
return this.api(method, path, body);
|
|
212
335
|
}
|
package/dist/GitLabProvider.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GitProvider } from "./GitProvider.ts";
|
|
2
|
-
import type { MRDetail, PullRequest, UserRef } from "./types.ts";
|
|
2
|
+
import type { BranchProtectionRule, CreatePullRequestInput, MRDetail, PullRequest, UpdatePullRequestInput, UserRef } from "./types.ts";
|
|
3
3
|
import { type ForgeLogger } from "./logger.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Strips the provider prefix from a scoped repositoryId and returns the
|
|
@@ -29,6 +29,11 @@ export declare class GitLabProvider implements GitProvider {
|
|
|
29
29
|
fetchSingleMR(projectPath: string, mrIid: number, currentUserNumericId: number | null): Promise<PullRequest | null>;
|
|
30
30
|
fetchPullRequests(): Promise<PullRequest[]>;
|
|
31
31
|
fetchMRDiscussions(repositoryId: string, mrIid: number): Promise<MRDetail>;
|
|
32
|
+
fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]>;
|
|
33
|
+
deleteBranch(projectPath: string, branch: string): Promise<void>;
|
|
34
|
+
fetchPullRequestByBranch(projectPath: string, sourceBranch: string): Promise<PullRequest | null>;
|
|
35
|
+
createPullRequest(input: CreatePullRequestInput): Promise<PullRequest>;
|
|
36
|
+
updatePullRequest(projectPath: string, mrIid: number, input: UpdatePullRequestInput): Promise<PullRequest>;
|
|
32
37
|
restRequest(method: string, path: string, body?: unknown): Promise<Response>;
|
|
33
38
|
private runQuery;
|
|
34
39
|
}
|
package/dist/GitLabProvider.js
CHANGED
|
@@ -313,6 +313,115 @@ class GitLabProvider {
|
|
|
313
313
|
const projectId = parseGitLabRepoId(repositoryId);
|
|
314
314
|
return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
|
|
315
315
|
}
|
|
316
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
317
|
+
const encoded = encodeURIComponent(projectPath);
|
|
318
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/protected_branches?per_page=100`, { headers: { "PRIVATE-TOKEN": this.token } });
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
321
|
+
}
|
|
322
|
+
const branches = await res.json();
|
|
323
|
+
return branches.map((b) => ({
|
|
324
|
+
pattern: b.name,
|
|
325
|
+
allowForcePush: b.allow_force_push,
|
|
326
|
+
allowDeletion: false,
|
|
327
|
+
requiredApprovals: 0,
|
|
328
|
+
requireStatusChecks: false,
|
|
329
|
+
raw: b
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
async deleteBranch(projectPath, branch) {
|
|
333
|
+
const encoded = encodeURIComponent(projectPath);
|
|
334
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/repository/branches/${encodeURIComponent(branch)}`, { method: "DELETE", headers: { "PRIVATE-TOKEN": this.token } });
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
340
|
+
const encoded = encodeURIComponent(projectPath);
|
|
341
|
+
const url = `${this.baseURL}/api/v4/projects/${encoded}/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened&per_page=1`;
|
|
342
|
+
const res = await fetch(url, {
|
|
343
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
344
|
+
});
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const mrs = await res.json();
|
|
350
|
+
if (!mrs[0])
|
|
351
|
+
return null;
|
|
352
|
+
return this.fetchSingleMR(projectPath, mrs[0].iid, null);
|
|
353
|
+
}
|
|
354
|
+
async createPullRequest(input) {
|
|
355
|
+
const encoded = encodeURIComponent(input.projectPath);
|
|
356
|
+
const body = {
|
|
357
|
+
source_branch: input.sourceBranch,
|
|
358
|
+
target_branch: input.targetBranch,
|
|
359
|
+
title: input.title
|
|
360
|
+
};
|
|
361
|
+
if (input.description != null)
|
|
362
|
+
body.description = input.description;
|
|
363
|
+
if (input.draft != null)
|
|
364
|
+
body.draft = input.draft;
|
|
365
|
+
if (input.labels?.length)
|
|
366
|
+
body.labels = input.labels.join(",");
|
|
367
|
+
if (input.assignees?.length)
|
|
368
|
+
body.assignee_ids = input.assignees;
|
|
369
|
+
if (input.reviewers?.length)
|
|
370
|
+
body.reviewer_ids = input.reviewers;
|
|
371
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests`, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: {
|
|
374
|
+
"PRIVATE-TOKEN": this.token,
|
|
375
|
+
"Content-Type": "application/json"
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify(body)
|
|
378
|
+
});
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
const text = await res.text();
|
|
381
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
382
|
+
}
|
|
383
|
+
const created = await res.json();
|
|
384
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.iid, null);
|
|
385
|
+
if (!pr)
|
|
386
|
+
throw new Error("Created MR but failed to fetch it back");
|
|
387
|
+
return pr;
|
|
388
|
+
}
|
|
389
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
390
|
+
const encoded = encodeURIComponent(projectPath);
|
|
391
|
+
const body = {};
|
|
392
|
+
if (input.title != null)
|
|
393
|
+
body.title = input.title;
|
|
394
|
+
if (input.description != null)
|
|
395
|
+
body.description = input.description;
|
|
396
|
+
if (input.draft != null)
|
|
397
|
+
body.draft = input.draft;
|
|
398
|
+
if (input.targetBranch != null)
|
|
399
|
+
body.target_branch = input.targetBranch;
|
|
400
|
+
if (input.labels)
|
|
401
|
+
body.labels = input.labels.join(",");
|
|
402
|
+
if (input.assignees)
|
|
403
|
+
body.assignee_ids = input.assignees;
|
|
404
|
+
if (input.reviewers)
|
|
405
|
+
body.reviewer_ids = input.reviewers;
|
|
406
|
+
if (input.stateEvent)
|
|
407
|
+
body.state_event = input.stateEvent;
|
|
408
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
|
|
409
|
+
method: "PUT",
|
|
410
|
+
headers: {
|
|
411
|
+
"PRIVATE-TOKEN": this.token,
|
|
412
|
+
"Content-Type": "application/json"
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify(body)
|
|
415
|
+
});
|
|
416
|
+
if (!res.ok) {
|
|
417
|
+
const text = await res.text();
|
|
418
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
419
|
+
}
|
|
420
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
421
|
+
if (!pr)
|
|
422
|
+
throw new Error("Updated MR but failed to fetch it back");
|
|
423
|
+
return pr;
|
|
424
|
+
}
|
|
316
425
|
async restRequest(method, path, body) {
|
|
317
426
|
const url = `${this.baseURL}${path}`;
|
|
318
427
|
const headers = {
|
package/dist/GitProvider.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MRDetail, PullRequest, UserRef } from "./types.ts";
|
|
1
|
+
import type { BranchProtectionRule, CreatePullRequestInput, MRDetail, PullRequest, UpdatePullRequestInput, UserRef } from "./types.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Provider-agnostic interface for a Git hosting service.
|
|
4
4
|
*
|
|
@@ -24,6 +24,31 @@ export interface GitProvider {
|
|
|
24
24
|
* Used by SubscriptionManager for real-time update handling.
|
|
25
25
|
*/
|
|
26
26
|
fetchSingleMR(projectPath: string, mrIid: number, currentUserNumericId: number | null): Promise<PullRequest | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Fetch a single MR/PR by its source branch within a project.
|
|
29
|
+
* Returns null if no open MR/PR exists for that branch.
|
|
30
|
+
*/
|
|
31
|
+
fetchPullRequestByBranch(projectPath: string, sourceBranch: string): Promise<PullRequest | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Create a new merge request / pull request.
|
|
34
|
+
* Returns the created PullRequest.
|
|
35
|
+
*/
|
|
36
|
+
createPullRequest(input: CreatePullRequestInput): Promise<PullRequest>;
|
|
37
|
+
/**
|
|
38
|
+
* Update an existing merge request / pull request.
|
|
39
|
+
* Returns the updated PullRequest.
|
|
40
|
+
*/
|
|
41
|
+
updatePullRequest(projectPath: string, mrIid: number, input: UpdatePullRequestInput): Promise<PullRequest>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch branch protection rules for a repository.
|
|
44
|
+
* Returns an array of rules (one per protected branch/pattern).
|
|
45
|
+
*/
|
|
46
|
+
fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Delete a branch from the repository.
|
|
49
|
+
* @throws if the branch doesn't exist or is protected.
|
|
50
|
+
*/
|
|
51
|
+
deleteBranch(projectPath: string, branch: string): Promise<void>;
|
|
27
52
|
/**
|
|
28
53
|
* Fetch discussions (comments, threads) for a specific MR/PR.
|
|
29
54
|
* Returns the MRDetail with discussions populated.
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* const provider = new GitLabProvider('https://gitlab.com', token, { logger: console });
|
|
12
12
|
* const prs = await provider.fetchPullRequests();
|
|
13
13
|
*/
|
|
14
|
-
export type { PullRequest, PullRequestsSnapshot, Pipeline, PipelineJob, UserRef, DiffStats, Discussion, Note, NoteAuthor, NotePosition, MRDetail, FeedEvent, FeedSnapshot, ServerNotification, } from "./types.ts";
|
|
14
|
+
export type { PullRequest, PullRequestsSnapshot, CreatePullRequestInput, UpdatePullRequestInput, BranchProtectionRule, Pipeline, PipelineJob, UserRef, DiffStats, Discussion, Note, NoteAuthor, NotePosition, MRDetail, FeedEvent, FeedSnapshot, ServerNotification, } from "./types.ts";
|
|
15
15
|
export type { GitProvider } from "./GitProvider.ts";
|
|
16
16
|
export { parseRepoId, repoIdProvider } from "./GitProvider.ts";
|
|
17
17
|
export type { ForgeLogger } from "./logger.ts";
|
package/dist/index.js
CHANGED
|
@@ -322,6 +322,115 @@ class GitLabProvider {
|
|
|
322
322
|
const projectId = parseGitLabRepoId(repositoryId);
|
|
323
323
|
return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
|
|
324
324
|
}
|
|
325
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
326
|
+
const encoded = encodeURIComponent(projectPath);
|
|
327
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/protected_branches?per_page=100`, { headers: { "PRIVATE-TOKEN": this.token } });
|
|
328
|
+
if (!res.ok) {
|
|
329
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
330
|
+
}
|
|
331
|
+
const branches = await res.json();
|
|
332
|
+
return branches.map((b) => ({
|
|
333
|
+
pattern: b.name,
|
|
334
|
+
allowForcePush: b.allow_force_push,
|
|
335
|
+
allowDeletion: false,
|
|
336
|
+
requiredApprovals: 0,
|
|
337
|
+
requireStatusChecks: false,
|
|
338
|
+
raw: b
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
async deleteBranch(projectPath, branch) {
|
|
342
|
+
const encoded = encodeURIComponent(projectPath);
|
|
343
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/repository/branches/${encodeURIComponent(branch)}`, { method: "DELETE", headers: { "PRIVATE-TOKEN": this.token } });
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
349
|
+
const encoded = encodeURIComponent(projectPath);
|
|
350
|
+
const url = `${this.baseURL}/api/v4/projects/${encoded}/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened&per_page=1`;
|
|
351
|
+
const res = await fetch(url, {
|
|
352
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
353
|
+
});
|
|
354
|
+
if (!res.ok) {
|
|
355
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const mrs = await res.json();
|
|
359
|
+
if (!mrs[0])
|
|
360
|
+
return null;
|
|
361
|
+
return this.fetchSingleMR(projectPath, mrs[0].iid, null);
|
|
362
|
+
}
|
|
363
|
+
async createPullRequest(input) {
|
|
364
|
+
const encoded = encodeURIComponent(input.projectPath);
|
|
365
|
+
const body = {
|
|
366
|
+
source_branch: input.sourceBranch,
|
|
367
|
+
target_branch: input.targetBranch,
|
|
368
|
+
title: input.title
|
|
369
|
+
};
|
|
370
|
+
if (input.description != null)
|
|
371
|
+
body.description = input.description;
|
|
372
|
+
if (input.draft != null)
|
|
373
|
+
body.draft = input.draft;
|
|
374
|
+
if (input.labels?.length)
|
|
375
|
+
body.labels = input.labels.join(",");
|
|
376
|
+
if (input.assignees?.length)
|
|
377
|
+
body.assignee_ids = input.assignees;
|
|
378
|
+
if (input.reviewers?.length)
|
|
379
|
+
body.reviewer_ids = input.reviewers;
|
|
380
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"PRIVATE-TOKEN": this.token,
|
|
384
|
+
"Content-Type": "application/json"
|
|
385
|
+
},
|
|
386
|
+
body: JSON.stringify(body)
|
|
387
|
+
});
|
|
388
|
+
if (!res.ok) {
|
|
389
|
+
const text = await res.text();
|
|
390
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
391
|
+
}
|
|
392
|
+
const created = await res.json();
|
|
393
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.iid, null);
|
|
394
|
+
if (!pr)
|
|
395
|
+
throw new Error("Created MR but failed to fetch it back");
|
|
396
|
+
return pr;
|
|
397
|
+
}
|
|
398
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
399
|
+
const encoded = encodeURIComponent(projectPath);
|
|
400
|
+
const body = {};
|
|
401
|
+
if (input.title != null)
|
|
402
|
+
body.title = input.title;
|
|
403
|
+
if (input.description != null)
|
|
404
|
+
body.description = input.description;
|
|
405
|
+
if (input.draft != null)
|
|
406
|
+
body.draft = input.draft;
|
|
407
|
+
if (input.targetBranch != null)
|
|
408
|
+
body.target_branch = input.targetBranch;
|
|
409
|
+
if (input.labels)
|
|
410
|
+
body.labels = input.labels.join(",");
|
|
411
|
+
if (input.assignees)
|
|
412
|
+
body.assignee_ids = input.assignees;
|
|
413
|
+
if (input.reviewers)
|
|
414
|
+
body.reviewer_ids = input.reviewers;
|
|
415
|
+
if (input.stateEvent)
|
|
416
|
+
body.state_event = input.stateEvent;
|
|
417
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
|
|
418
|
+
method: "PUT",
|
|
419
|
+
headers: {
|
|
420
|
+
"PRIVATE-TOKEN": this.token,
|
|
421
|
+
"Content-Type": "application/json"
|
|
422
|
+
},
|
|
423
|
+
body: JSON.stringify(body)
|
|
424
|
+
});
|
|
425
|
+
if (!res.ok) {
|
|
426
|
+
const text = await res.text();
|
|
427
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
428
|
+
}
|
|
429
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
430
|
+
if (!pr)
|
|
431
|
+
throw new Error("Updated MR but failed to fetch it back");
|
|
432
|
+
return pr;
|
|
433
|
+
}
|
|
325
434
|
async restRequest(method, path, body) {
|
|
326
435
|
const url = `${this.baseURL}${path}`;
|
|
327
436
|
const headers = {
|
|
@@ -563,6 +672,129 @@ class GitHubProvider {
|
|
|
563
672
|
}
|
|
564
673
|
return { mrIid, repositoryId, discussions };
|
|
565
674
|
}
|
|
675
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
676
|
+
const res = await this.api("GET", `/repos/${projectPath}/branches?protected=true&per_page=100`);
|
|
677
|
+
if (!res.ok) {
|
|
678
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
679
|
+
}
|
|
680
|
+
const branches = await res.json();
|
|
681
|
+
const rules = [];
|
|
682
|
+
for (const b of branches) {
|
|
683
|
+
if (!b.protected)
|
|
684
|
+
continue;
|
|
685
|
+
const detailRes = await this.api("GET", `/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`);
|
|
686
|
+
if (!detailRes.ok) {
|
|
687
|
+
rules.push({
|
|
688
|
+
pattern: b.name,
|
|
689
|
+
allowForcePush: false,
|
|
690
|
+
allowDeletion: false,
|
|
691
|
+
requiredApprovals: 0,
|
|
692
|
+
requireStatusChecks: false
|
|
693
|
+
});
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const detail = await detailRes.json();
|
|
697
|
+
rules.push({
|
|
698
|
+
pattern: b.name,
|
|
699
|
+
allowForcePush: detail.allow_force_pushes?.enabled ?? false,
|
|
700
|
+
allowDeletion: detail.allow_deletions?.enabled ?? false,
|
|
701
|
+
requiredApprovals: detail.required_pull_request_reviews?.required_approving_review_count ?? 0,
|
|
702
|
+
requireStatusChecks: detail.required_status_checks !== null && detail.required_status_checks !== undefined,
|
|
703
|
+
raw: detail
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return rules;
|
|
707
|
+
}
|
|
708
|
+
async deleteBranch(projectPath, branch) {
|
|
709
|
+
const res = await this.api("DELETE", `/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`);
|
|
710
|
+
if (!res.ok) {
|
|
711
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
715
|
+
const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
|
|
716
|
+
if (!res.ok) {
|
|
717
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const prs = await res.json();
|
|
721
|
+
if (!prs[0])
|
|
722
|
+
return null;
|
|
723
|
+
return this.fetchSingleMR(projectPath, prs[0].number, null);
|
|
724
|
+
}
|
|
725
|
+
async createPullRequest(input) {
|
|
726
|
+
const body = {
|
|
727
|
+
head: input.sourceBranch,
|
|
728
|
+
base: input.targetBranch,
|
|
729
|
+
title: input.title
|
|
730
|
+
};
|
|
731
|
+
if (input.description != null)
|
|
732
|
+
body.body = input.description;
|
|
733
|
+
if (input.draft != null)
|
|
734
|
+
body.draft = input.draft;
|
|
735
|
+
const res = await this.api("POST", `/repos/${input.projectPath}/pulls`, body);
|
|
736
|
+
if (!res.ok) {
|
|
737
|
+
const text = await res.text();
|
|
738
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
739
|
+
}
|
|
740
|
+
const created = await res.json();
|
|
741
|
+
if (input.reviewers?.length) {
|
|
742
|
+
await this.api("POST", `/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`, {
|
|
743
|
+
reviewers: input.reviewers
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (input.assignees?.length) {
|
|
747
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/assignees`, {
|
|
748
|
+
assignees: input.assignees
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
if (input.labels?.length) {
|
|
752
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/labels`, {
|
|
753
|
+
labels: input.labels
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.number, null);
|
|
757
|
+
if (!pr)
|
|
758
|
+
throw new Error("Created PR but failed to fetch it back");
|
|
759
|
+
return pr;
|
|
760
|
+
}
|
|
761
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
762
|
+
const body = {};
|
|
763
|
+
if (input.title != null)
|
|
764
|
+
body.title = input.title;
|
|
765
|
+
if (input.description != null)
|
|
766
|
+
body.body = input.description;
|
|
767
|
+
if (input.draft != null)
|
|
768
|
+
body.draft = input.draft;
|
|
769
|
+
if (input.targetBranch != null)
|
|
770
|
+
body.base = input.targetBranch;
|
|
771
|
+
if (input.stateEvent)
|
|
772
|
+
body.state = input.stateEvent === "close" ? "closed" : "open";
|
|
773
|
+
const res = await this.api("PATCH", `/repos/${projectPath}/pulls/${mrIid}`, body);
|
|
774
|
+
if (!res.ok) {
|
|
775
|
+
const text = await res.text();
|
|
776
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
777
|
+
}
|
|
778
|
+
if (input.reviewers) {
|
|
779
|
+
await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, {
|
|
780
|
+
reviewers: input.reviewers
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (input.assignees) {
|
|
784
|
+
await this.api("POST", `/repos/${projectPath}/issues/${mrIid}/assignees`, {
|
|
785
|
+
assignees: input.assignees
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (input.labels) {
|
|
789
|
+
await this.api("PUT", `/repos/${projectPath}/issues/${mrIid}/labels`, {
|
|
790
|
+
labels: input.labels
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
794
|
+
if (!pr)
|
|
795
|
+
throw new Error("Updated PR but failed to fetch it back");
|
|
796
|
+
return pr;
|
|
797
|
+
}
|
|
566
798
|
async restRequest(method, path, body) {
|
|
567
799
|
return this.api(method, path, body);
|
|
568
800
|
}
|
package/dist/providers.js
CHANGED
|
@@ -313,6 +313,115 @@ class GitLabProvider {
|
|
|
313
313
|
const projectId = parseGitLabRepoId(repositoryId);
|
|
314
314
|
return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
|
|
315
315
|
}
|
|
316
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
317
|
+
const encoded = encodeURIComponent(projectPath);
|
|
318
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/protected_branches?per_page=100`, { headers: { "PRIVATE-TOKEN": this.token } });
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
321
|
+
}
|
|
322
|
+
const branches = await res.json();
|
|
323
|
+
return branches.map((b) => ({
|
|
324
|
+
pattern: b.name,
|
|
325
|
+
allowForcePush: b.allow_force_push,
|
|
326
|
+
allowDeletion: false,
|
|
327
|
+
requiredApprovals: 0,
|
|
328
|
+
requireStatusChecks: false,
|
|
329
|
+
raw: b
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
async deleteBranch(projectPath, branch) {
|
|
333
|
+
const encoded = encodeURIComponent(projectPath);
|
|
334
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/repository/branches/${encodeURIComponent(branch)}`, { method: "DELETE", headers: { "PRIVATE-TOKEN": this.token } });
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
340
|
+
const encoded = encodeURIComponent(projectPath);
|
|
341
|
+
const url = `${this.baseURL}/api/v4/projects/${encoded}/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened&per_page=1`;
|
|
342
|
+
const res = await fetch(url, {
|
|
343
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
344
|
+
});
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const mrs = await res.json();
|
|
350
|
+
if (!mrs[0])
|
|
351
|
+
return null;
|
|
352
|
+
return this.fetchSingleMR(projectPath, mrs[0].iid, null);
|
|
353
|
+
}
|
|
354
|
+
async createPullRequest(input) {
|
|
355
|
+
const encoded = encodeURIComponent(input.projectPath);
|
|
356
|
+
const body = {
|
|
357
|
+
source_branch: input.sourceBranch,
|
|
358
|
+
target_branch: input.targetBranch,
|
|
359
|
+
title: input.title
|
|
360
|
+
};
|
|
361
|
+
if (input.description != null)
|
|
362
|
+
body.description = input.description;
|
|
363
|
+
if (input.draft != null)
|
|
364
|
+
body.draft = input.draft;
|
|
365
|
+
if (input.labels?.length)
|
|
366
|
+
body.labels = input.labels.join(",");
|
|
367
|
+
if (input.assignees?.length)
|
|
368
|
+
body.assignee_ids = input.assignees;
|
|
369
|
+
if (input.reviewers?.length)
|
|
370
|
+
body.reviewer_ids = input.reviewers;
|
|
371
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests`, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: {
|
|
374
|
+
"PRIVATE-TOKEN": this.token,
|
|
375
|
+
"Content-Type": "application/json"
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify(body)
|
|
378
|
+
});
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
const text = await res.text();
|
|
381
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
382
|
+
}
|
|
383
|
+
const created = await res.json();
|
|
384
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.iid, null);
|
|
385
|
+
if (!pr)
|
|
386
|
+
throw new Error("Created MR but failed to fetch it back");
|
|
387
|
+
return pr;
|
|
388
|
+
}
|
|
389
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
390
|
+
const encoded = encodeURIComponent(projectPath);
|
|
391
|
+
const body = {};
|
|
392
|
+
if (input.title != null)
|
|
393
|
+
body.title = input.title;
|
|
394
|
+
if (input.description != null)
|
|
395
|
+
body.description = input.description;
|
|
396
|
+
if (input.draft != null)
|
|
397
|
+
body.draft = input.draft;
|
|
398
|
+
if (input.targetBranch != null)
|
|
399
|
+
body.target_branch = input.targetBranch;
|
|
400
|
+
if (input.labels)
|
|
401
|
+
body.labels = input.labels.join(",");
|
|
402
|
+
if (input.assignees)
|
|
403
|
+
body.assignee_ids = input.assignees;
|
|
404
|
+
if (input.reviewers)
|
|
405
|
+
body.reviewer_ids = input.reviewers;
|
|
406
|
+
if (input.stateEvent)
|
|
407
|
+
body.state_event = input.stateEvent;
|
|
408
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
|
|
409
|
+
method: "PUT",
|
|
410
|
+
headers: {
|
|
411
|
+
"PRIVATE-TOKEN": this.token,
|
|
412
|
+
"Content-Type": "application/json"
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify(body)
|
|
415
|
+
});
|
|
416
|
+
if (!res.ok) {
|
|
417
|
+
const text = await res.text();
|
|
418
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
419
|
+
}
|
|
420
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
421
|
+
if (!pr)
|
|
422
|
+
throw new Error("Updated MR but failed to fetch it back");
|
|
423
|
+
return pr;
|
|
424
|
+
}
|
|
316
425
|
async restRequest(method, path, body) {
|
|
317
426
|
const url = `${this.baseURL}${path}`;
|
|
318
427
|
const headers = {
|
|
@@ -554,6 +663,129 @@ class GitHubProvider {
|
|
|
554
663
|
}
|
|
555
664
|
return { mrIid, repositoryId, discussions };
|
|
556
665
|
}
|
|
666
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
667
|
+
const res = await this.api("GET", `/repos/${projectPath}/branches?protected=true&per_page=100`);
|
|
668
|
+
if (!res.ok) {
|
|
669
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
670
|
+
}
|
|
671
|
+
const branches = await res.json();
|
|
672
|
+
const rules = [];
|
|
673
|
+
for (const b of branches) {
|
|
674
|
+
if (!b.protected)
|
|
675
|
+
continue;
|
|
676
|
+
const detailRes = await this.api("GET", `/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`);
|
|
677
|
+
if (!detailRes.ok) {
|
|
678
|
+
rules.push({
|
|
679
|
+
pattern: b.name,
|
|
680
|
+
allowForcePush: false,
|
|
681
|
+
allowDeletion: false,
|
|
682
|
+
requiredApprovals: 0,
|
|
683
|
+
requireStatusChecks: false
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const detail = await detailRes.json();
|
|
688
|
+
rules.push({
|
|
689
|
+
pattern: b.name,
|
|
690
|
+
allowForcePush: detail.allow_force_pushes?.enabled ?? false,
|
|
691
|
+
allowDeletion: detail.allow_deletions?.enabled ?? false,
|
|
692
|
+
requiredApprovals: detail.required_pull_request_reviews?.required_approving_review_count ?? 0,
|
|
693
|
+
requireStatusChecks: detail.required_status_checks !== null && detail.required_status_checks !== undefined,
|
|
694
|
+
raw: detail
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return rules;
|
|
698
|
+
}
|
|
699
|
+
async deleteBranch(projectPath, branch) {
|
|
700
|
+
const res = await this.api("DELETE", `/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`);
|
|
701
|
+
if (!res.ok) {
|
|
702
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
706
|
+
const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
|
|
707
|
+
if (!res.ok) {
|
|
708
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const prs = await res.json();
|
|
712
|
+
if (!prs[0])
|
|
713
|
+
return null;
|
|
714
|
+
return this.fetchSingleMR(projectPath, prs[0].number, null);
|
|
715
|
+
}
|
|
716
|
+
async createPullRequest(input) {
|
|
717
|
+
const body = {
|
|
718
|
+
head: input.sourceBranch,
|
|
719
|
+
base: input.targetBranch,
|
|
720
|
+
title: input.title
|
|
721
|
+
};
|
|
722
|
+
if (input.description != null)
|
|
723
|
+
body.body = input.description;
|
|
724
|
+
if (input.draft != null)
|
|
725
|
+
body.draft = input.draft;
|
|
726
|
+
const res = await this.api("POST", `/repos/${input.projectPath}/pulls`, body);
|
|
727
|
+
if (!res.ok) {
|
|
728
|
+
const text = await res.text();
|
|
729
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
730
|
+
}
|
|
731
|
+
const created = await res.json();
|
|
732
|
+
if (input.reviewers?.length) {
|
|
733
|
+
await this.api("POST", `/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`, {
|
|
734
|
+
reviewers: input.reviewers
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (input.assignees?.length) {
|
|
738
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/assignees`, {
|
|
739
|
+
assignees: input.assignees
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
if (input.labels?.length) {
|
|
743
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/labels`, {
|
|
744
|
+
labels: input.labels
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.number, null);
|
|
748
|
+
if (!pr)
|
|
749
|
+
throw new Error("Created PR but failed to fetch it back");
|
|
750
|
+
return pr;
|
|
751
|
+
}
|
|
752
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
753
|
+
const body = {};
|
|
754
|
+
if (input.title != null)
|
|
755
|
+
body.title = input.title;
|
|
756
|
+
if (input.description != null)
|
|
757
|
+
body.body = input.description;
|
|
758
|
+
if (input.draft != null)
|
|
759
|
+
body.draft = input.draft;
|
|
760
|
+
if (input.targetBranch != null)
|
|
761
|
+
body.base = input.targetBranch;
|
|
762
|
+
if (input.stateEvent)
|
|
763
|
+
body.state = input.stateEvent === "close" ? "closed" : "open";
|
|
764
|
+
const res = await this.api("PATCH", `/repos/${projectPath}/pulls/${mrIid}`, body);
|
|
765
|
+
if (!res.ok) {
|
|
766
|
+
const text = await res.text();
|
|
767
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
768
|
+
}
|
|
769
|
+
if (input.reviewers) {
|
|
770
|
+
await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, {
|
|
771
|
+
reviewers: input.reviewers
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
if (input.assignees) {
|
|
775
|
+
await this.api("POST", `/repos/${projectPath}/issues/${mrIid}/assignees`, {
|
|
776
|
+
assignees: input.assignees
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (input.labels) {
|
|
780
|
+
await this.api("PUT", `/repos/${projectPath}/issues/${mrIid}/labels`, {
|
|
781
|
+
labels: input.labels
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
785
|
+
if (!pr)
|
|
786
|
+
throw new Error("Updated PR but failed to fetch it back");
|
|
787
|
+
return pr;
|
|
788
|
+
}
|
|
557
789
|
async restRequest(method, path, body) {
|
|
558
790
|
return this.api(method, path, body);
|
|
559
791
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -76,6 +76,54 @@ export interface PullRequest {
|
|
|
76
76
|
*/
|
|
77
77
|
detailedMergeStatus: string | null;
|
|
78
78
|
}
|
|
79
|
+
/** Input for creating a new merge request / pull request. */
|
|
80
|
+
export interface CreatePullRequestInput {
|
|
81
|
+
/** Project path (GitLab: "group/project") or owner/repo (GitHub: "owner/repo"). */
|
|
82
|
+
projectPath: string;
|
|
83
|
+
title: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
sourceBranch: string;
|
|
86
|
+
targetBranch: string;
|
|
87
|
+
draft?: boolean;
|
|
88
|
+
/** Usernames to assign as reviewers. */
|
|
89
|
+
reviewers?: string[];
|
|
90
|
+
/** Usernames to assign. */
|
|
91
|
+
assignees?: string[];
|
|
92
|
+
/** Labels to apply (string names). */
|
|
93
|
+
labels?: string[];
|
|
94
|
+
}
|
|
95
|
+
/** Input for updating an existing merge request / pull request. */
|
|
96
|
+
export interface UpdatePullRequestInput {
|
|
97
|
+
title?: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
/** Set draft status. */
|
|
100
|
+
draft?: boolean;
|
|
101
|
+
/** Change the target branch. */
|
|
102
|
+
targetBranch?: string;
|
|
103
|
+
/** Usernames to assign as reviewers (replaces current set). */
|
|
104
|
+
reviewers?: string[];
|
|
105
|
+
/** Usernames to assign (replaces current set). */
|
|
106
|
+
assignees?: string[];
|
|
107
|
+
/** Labels (replaces current set). */
|
|
108
|
+
labels?: string[];
|
|
109
|
+
/** Set MR state: "close" or "reopen". */
|
|
110
|
+
stateEvent?: "close" | "reopen";
|
|
111
|
+
}
|
|
112
|
+
/** Branch protection rule (provider-agnostic). */
|
|
113
|
+
export interface BranchProtectionRule {
|
|
114
|
+
/** Branch name or pattern, e.g. "main" or "release/*". */
|
|
115
|
+
pattern: string;
|
|
116
|
+
/** Whether force-pushes are allowed. */
|
|
117
|
+
allowForcePush: boolean;
|
|
118
|
+
/** Whether branch deletions are allowed. */
|
|
119
|
+
allowDeletion: boolean;
|
|
120
|
+
/** Number of required approving reviews (0 = none required). */
|
|
121
|
+
requiredApprovals: number;
|
|
122
|
+
/** Whether the branch requires status checks to pass before merge. */
|
|
123
|
+
requireStatusChecks: boolean;
|
|
124
|
+
/** Raw provider-specific data for fields not covered above. */
|
|
125
|
+
raw?: Record<string, unknown>;
|
|
126
|
+
}
|
|
79
127
|
/** Snapshot payload sent when a client first connects. */
|
|
80
128
|
export interface PullRequestsSnapshot {
|
|
81
129
|
items: PullRequest[];
|
package/package.json
CHANGED
package/src/GitHubProvider.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import type { GitProvider } from "./GitProvider.ts";
|
|
16
16
|
import type {
|
|
17
|
+
BranchProtectionRule,
|
|
18
|
+
CreatePullRequestInput,
|
|
17
19
|
DiffStats,
|
|
18
20
|
Discussion,
|
|
19
21
|
MRDetail,
|
|
@@ -23,6 +25,7 @@ import type {
|
|
|
23
25
|
Pipeline,
|
|
24
26
|
PipelineJob,
|
|
25
27
|
PullRequest,
|
|
28
|
+
UpdatePullRequestInput,
|
|
26
29
|
UserRef,
|
|
27
30
|
} from "./types.ts";
|
|
28
31
|
import { type ForgeLogger, noopLogger } from "./logger.ts";
|
|
@@ -398,6 +401,152 @@ export class GitHubProvider implements GitProvider {
|
|
|
398
401
|
return { mrIid, repositoryId, discussions };
|
|
399
402
|
}
|
|
400
403
|
|
|
404
|
+
async fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]> {
|
|
405
|
+
const res = await this.api("GET", `/repos/${projectPath}/branches?protected=true&per_page=100`);
|
|
406
|
+
if (!res.ok) {
|
|
407
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
408
|
+
}
|
|
409
|
+
const branches = (await res.json()) as Array<{
|
|
410
|
+
name: string;
|
|
411
|
+
protected: boolean;
|
|
412
|
+
protection?: {
|
|
413
|
+
enabled: boolean;
|
|
414
|
+
required_status_checks?: { enforcement_level: string; contexts: string[] } | null;
|
|
415
|
+
};
|
|
416
|
+
}>;
|
|
417
|
+
|
|
418
|
+
const rules: BranchProtectionRule[] = [];
|
|
419
|
+
for (const b of branches) {
|
|
420
|
+
if (!b.protected) continue;
|
|
421
|
+
// Fetch detailed protection for each protected branch
|
|
422
|
+
const detailRes = await this.api("GET", `/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`);
|
|
423
|
+
if (!detailRes.ok) {
|
|
424
|
+
rules.push({
|
|
425
|
+
pattern: b.name,
|
|
426
|
+
allowForcePush: false,
|
|
427
|
+
allowDeletion: false,
|
|
428
|
+
requiredApprovals: 0,
|
|
429
|
+
requireStatusChecks: false,
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const detail = (await detailRes.json()) as {
|
|
434
|
+
allow_force_pushes?: { enabled: boolean };
|
|
435
|
+
allow_deletions?: { enabled: boolean };
|
|
436
|
+
required_pull_request_reviews?: { required_approving_review_count?: number } | null;
|
|
437
|
+
required_status_checks?: { strict: boolean; contexts: string[] } | null;
|
|
438
|
+
};
|
|
439
|
+
rules.push({
|
|
440
|
+
pattern: b.name,
|
|
441
|
+
allowForcePush: detail.allow_force_pushes?.enabled ?? false,
|
|
442
|
+
allowDeletion: detail.allow_deletions?.enabled ?? false,
|
|
443
|
+
requiredApprovals: detail.required_pull_request_reviews?.required_approving_review_count ?? 0,
|
|
444
|
+
requireStatusChecks: detail.required_status_checks !== null && detail.required_status_checks !== undefined,
|
|
445
|
+
raw: detail as unknown as Record<string, unknown>,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return rules;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async deleteBranch(projectPath: string, branch: string): Promise<void> {
|
|
452
|
+
const res = await this.api("DELETE", `/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`);
|
|
453
|
+
if (!res.ok) {
|
|
454
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async fetchPullRequestByBranch(
|
|
459
|
+
projectPath: string,
|
|
460
|
+
sourceBranch: string,
|
|
461
|
+
): Promise<PullRequest | null> {
|
|
462
|
+
const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
|
|
463
|
+
if (!res.ok) {
|
|
464
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const prs = (await res.json()) as GHPullRequest[];
|
|
468
|
+
if (!prs[0]) return null;
|
|
469
|
+
return this.fetchSingleMR(projectPath, prs[0].number, null);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async createPullRequest(input: CreatePullRequestInput): Promise<PullRequest> {
|
|
473
|
+
const body: Record<string, unknown> = {
|
|
474
|
+
head: input.sourceBranch,
|
|
475
|
+
base: input.targetBranch,
|
|
476
|
+
title: input.title,
|
|
477
|
+
};
|
|
478
|
+
if (input.description != null) body.body = input.description;
|
|
479
|
+
if (input.draft != null) body.draft = input.draft;
|
|
480
|
+
|
|
481
|
+
const res = await this.api("POST", `/repos/${input.projectPath}/pulls`, body);
|
|
482
|
+
if (!res.ok) {
|
|
483
|
+
const text = await res.text();
|
|
484
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
485
|
+
}
|
|
486
|
+
const created = (await res.json()) as GHPullRequest;
|
|
487
|
+
|
|
488
|
+
// GitHub doesn't support reviewers/assignees/labels on create — add them separately
|
|
489
|
+
if (input.reviewers?.length) {
|
|
490
|
+
await this.api("POST", `/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`, {
|
|
491
|
+
reviewers: input.reviewers,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (input.assignees?.length) {
|
|
495
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/assignees`, {
|
|
496
|
+
assignees: input.assignees,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (input.labels?.length) {
|
|
500
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/labels`, {
|
|
501
|
+
labels: input.labels,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.number, null);
|
|
506
|
+
if (!pr) throw new Error("Created PR but failed to fetch it back");
|
|
507
|
+
return pr;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async updatePullRequest(
|
|
511
|
+
projectPath: string,
|
|
512
|
+
mrIid: number,
|
|
513
|
+
input: UpdatePullRequestInput,
|
|
514
|
+
): Promise<PullRequest> {
|
|
515
|
+
const body: Record<string, unknown> = {};
|
|
516
|
+
if (input.title != null) body.title = input.title;
|
|
517
|
+
if (input.description != null) body.body = input.description;
|
|
518
|
+
if (input.draft != null) body.draft = input.draft;
|
|
519
|
+
if (input.targetBranch != null) body.base = input.targetBranch;
|
|
520
|
+
if (input.stateEvent) body.state = input.stateEvent === "close" ? "closed" : "open";
|
|
521
|
+
|
|
522
|
+
const res = await this.api("PATCH", `/repos/${projectPath}/pulls/${mrIid}`, body);
|
|
523
|
+
if (!res.ok) {
|
|
524
|
+
const text = await res.text();
|
|
525
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Handle reviewers/assignees/labels replacement if provided
|
|
529
|
+
if (input.reviewers) {
|
|
530
|
+
await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, {
|
|
531
|
+
reviewers: input.reviewers,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (input.assignees) {
|
|
535
|
+
await this.api("POST", `/repos/${projectPath}/issues/${mrIid}/assignees`, {
|
|
536
|
+
assignees: input.assignees,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (input.labels) {
|
|
540
|
+
await this.api("PUT", `/repos/${projectPath}/issues/${mrIid}/labels`, {
|
|
541
|
+
labels: input.labels,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
546
|
+
if (!pr) throw new Error("Updated PR but failed to fetch it back");
|
|
547
|
+
return pr;
|
|
548
|
+
}
|
|
549
|
+
|
|
401
550
|
async restRequest(
|
|
402
551
|
method: string,
|
|
403
552
|
path: string,
|
package/src/GitLabProvider.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { GitProvider } from "./GitProvider.ts";
|
|
2
2
|
import type {
|
|
3
|
+
BranchProtectionRule,
|
|
4
|
+
CreatePullRequestInput,
|
|
3
5
|
DiffStats,
|
|
4
6
|
MRDetail,
|
|
5
7
|
Pipeline,
|
|
6
8
|
PipelineJob,
|
|
7
9
|
PullRequest,
|
|
10
|
+
UpdatePullRequestInput,
|
|
8
11
|
UserRef,
|
|
9
12
|
} from "./types.ts";
|
|
10
13
|
import { type ForgeLogger, noopLogger } from "./logger.ts";
|
|
@@ -419,6 +422,128 @@ export class GitLabProvider implements GitProvider {
|
|
|
419
422
|
return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
async fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]> {
|
|
426
|
+
const encoded = encodeURIComponent(projectPath);
|
|
427
|
+
const res = await fetch(
|
|
428
|
+
`${this.baseURL}/api/v4/projects/${encoded}/protected_branches?per_page=100`,
|
|
429
|
+
{ headers: { "PRIVATE-TOKEN": this.token } },
|
|
430
|
+
);
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
433
|
+
}
|
|
434
|
+
const branches = (await res.json()) as Array<{
|
|
435
|
+
name: string;
|
|
436
|
+
allow_force_push: boolean;
|
|
437
|
+
push_access_levels: Array<{ access_level: number }>;
|
|
438
|
+
merge_access_levels: Array<{ access_level: number }>;
|
|
439
|
+
code_owner_approval_required?: boolean;
|
|
440
|
+
}>;
|
|
441
|
+
return branches.map((b) => ({
|
|
442
|
+
pattern: b.name,
|
|
443
|
+
allowForcePush: b.allow_force_push,
|
|
444
|
+
allowDeletion: false,
|
|
445
|
+
requiredApprovals: 0,
|
|
446
|
+
requireStatusChecks: false,
|
|
447
|
+
raw: b as unknown as Record<string, unknown>,
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async deleteBranch(projectPath: string, branch: string): Promise<void> {
|
|
452
|
+
const encoded = encodeURIComponent(projectPath);
|
|
453
|
+
const res = await fetch(
|
|
454
|
+
`${this.baseURL}/api/v4/projects/${encoded}/repository/branches/${encodeURIComponent(branch)}`,
|
|
455
|
+
{ method: "DELETE", headers: { "PRIVATE-TOKEN": this.token } },
|
|
456
|
+
);
|
|
457
|
+
if (!res.ok) {
|
|
458
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async fetchPullRequestByBranch(
|
|
463
|
+
projectPath: string,
|
|
464
|
+
sourceBranch: string,
|
|
465
|
+
): Promise<PullRequest | null> {
|
|
466
|
+
const encoded = encodeURIComponent(projectPath);
|
|
467
|
+
const url = `${this.baseURL}/api/v4/projects/${encoded}/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened&per_page=1`;
|
|
468
|
+
const res = await fetch(url, {
|
|
469
|
+
headers: { "PRIVATE-TOKEN": this.token },
|
|
470
|
+
});
|
|
471
|
+
if (!res.ok) {
|
|
472
|
+
this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const mrs = (await res.json()) as Array<{ iid: number }>;
|
|
476
|
+
if (!mrs[0]) return null;
|
|
477
|
+
return this.fetchSingleMR(projectPath, mrs[0].iid, null);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async createPullRequest(input: CreatePullRequestInput): Promise<PullRequest> {
|
|
481
|
+
const encoded = encodeURIComponent(input.projectPath);
|
|
482
|
+
const body: Record<string, unknown> = {
|
|
483
|
+
source_branch: input.sourceBranch,
|
|
484
|
+
target_branch: input.targetBranch,
|
|
485
|
+
title: input.title,
|
|
486
|
+
};
|
|
487
|
+
if (input.description != null) body.description = input.description;
|
|
488
|
+
if (input.draft != null) body.draft = input.draft;
|
|
489
|
+
if (input.labels?.length) body.labels = input.labels.join(",");
|
|
490
|
+
if (input.assignees?.length) body.assignee_ids = input.assignees;
|
|
491
|
+
if (input.reviewers?.length) body.reviewer_ids = input.reviewers;
|
|
492
|
+
|
|
493
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests`, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: {
|
|
496
|
+
"PRIVATE-TOKEN": this.token,
|
|
497
|
+
"Content-Type": "application/json",
|
|
498
|
+
},
|
|
499
|
+
body: JSON.stringify(body),
|
|
500
|
+
});
|
|
501
|
+
if (!res.ok) {
|
|
502
|
+
const text = await res.text();
|
|
503
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
504
|
+
}
|
|
505
|
+
const created = (await res.json()) as { iid: number };
|
|
506
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.iid, null);
|
|
507
|
+
if (!pr) throw new Error("Created MR but failed to fetch it back");
|
|
508
|
+
return pr;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async updatePullRequest(
|
|
512
|
+
projectPath: string,
|
|
513
|
+
mrIid: number,
|
|
514
|
+
input: UpdatePullRequestInput,
|
|
515
|
+
): Promise<PullRequest> {
|
|
516
|
+
const encoded = encodeURIComponent(projectPath);
|
|
517
|
+
const body: Record<string, unknown> = {};
|
|
518
|
+
if (input.title != null) body.title = input.title;
|
|
519
|
+
if (input.description != null) body.description = input.description;
|
|
520
|
+
if (input.draft != null) body.draft = input.draft;
|
|
521
|
+
if (input.targetBranch != null) body.target_branch = input.targetBranch;
|
|
522
|
+
if (input.labels) body.labels = input.labels.join(",");
|
|
523
|
+
if (input.assignees) body.assignee_ids = input.assignees;
|
|
524
|
+
if (input.reviewers) body.reviewer_ids = input.reviewers;
|
|
525
|
+
if (input.stateEvent) body.state_event = input.stateEvent;
|
|
526
|
+
|
|
527
|
+
const res = await fetch(
|
|
528
|
+
`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`,
|
|
529
|
+
{
|
|
530
|
+
method: "PUT",
|
|
531
|
+
headers: {
|
|
532
|
+
"PRIVATE-TOKEN": this.token,
|
|
533
|
+
"Content-Type": "application/json",
|
|
534
|
+
},
|
|
535
|
+
body: JSON.stringify(body),
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
if (!res.ok) {
|
|
539
|
+
const text = await res.text();
|
|
540
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
541
|
+
}
|
|
542
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
543
|
+
if (!pr) throw new Error("Updated MR but failed to fetch it back");
|
|
544
|
+
return pr;
|
|
545
|
+
}
|
|
546
|
+
|
|
422
547
|
async restRequest(method: string, path: string, body?: unknown): Promise<Response> {
|
|
423
548
|
const url = `${this.baseURL}${path}`;
|
|
424
549
|
const headers: Record<string, string> = {
|
package/src/GitProvider.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BranchProtectionRule,
|
|
3
|
+
CreatePullRequestInput,
|
|
4
|
+
Discussion,
|
|
5
|
+
MRDetail,
|
|
6
|
+
PullRequest,
|
|
7
|
+
UpdatePullRequestInput,
|
|
8
|
+
UserRef,
|
|
9
|
+
} from "./types.ts";
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
12
|
* Provider-agnostic interface for a Git hosting service.
|
|
@@ -34,6 +42,48 @@ export interface GitProvider {
|
|
|
34
42
|
currentUserNumericId: number | null,
|
|
35
43
|
): Promise<PullRequest | null>;
|
|
36
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Fetch a single MR/PR by its source branch within a project.
|
|
47
|
+
* Returns null if no open MR/PR exists for that branch.
|
|
48
|
+
*/
|
|
49
|
+
fetchPullRequestByBranch(
|
|
50
|
+
projectPath: string,
|
|
51
|
+
sourceBranch: string,
|
|
52
|
+
): Promise<PullRequest | null>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a new merge request / pull request.
|
|
56
|
+
* Returns the created PullRequest.
|
|
57
|
+
*/
|
|
58
|
+
createPullRequest(input: CreatePullRequestInput): Promise<PullRequest>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update an existing merge request / pull request.
|
|
62
|
+
* Returns the updated PullRequest.
|
|
63
|
+
*/
|
|
64
|
+
updatePullRequest(
|
|
65
|
+
projectPath: string,
|
|
66
|
+
mrIid: number,
|
|
67
|
+
input: UpdatePullRequestInput,
|
|
68
|
+
): Promise<PullRequest>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fetch branch protection rules for a repository.
|
|
72
|
+
* Returns an array of rules (one per protected branch/pattern).
|
|
73
|
+
*/
|
|
74
|
+
fetchBranchProtectionRules(
|
|
75
|
+
projectPath: string,
|
|
76
|
+
): Promise<BranchProtectionRule[]>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete a branch from the repository.
|
|
80
|
+
* @throws if the branch doesn't exist or is protected.
|
|
81
|
+
*/
|
|
82
|
+
deleteBranch(
|
|
83
|
+
projectPath: string,
|
|
84
|
+
branch: string,
|
|
85
|
+
): Promise<void>;
|
|
86
|
+
|
|
37
87
|
/**
|
|
38
88
|
* Fetch discussions (comments, threads) for a specific MR/PR.
|
|
39
89
|
* Returns the MRDetail with discussions populated.
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -84,6 +84,59 @@ export interface PullRequest {
|
|
|
84
84
|
detailedMergeStatus: string | null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// ── MR/PR mutation inputs ─────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/** Input for creating a new merge request / pull request. */
|
|
90
|
+
export interface CreatePullRequestInput {
|
|
91
|
+
/** Project path (GitLab: "group/project") or owner/repo (GitHub: "owner/repo"). */
|
|
92
|
+
projectPath: string;
|
|
93
|
+
title: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
sourceBranch: string;
|
|
96
|
+
targetBranch: string;
|
|
97
|
+
draft?: boolean;
|
|
98
|
+
/** Usernames to assign as reviewers. */
|
|
99
|
+
reviewers?: string[];
|
|
100
|
+
/** Usernames to assign. */
|
|
101
|
+
assignees?: string[];
|
|
102
|
+
/** Labels to apply (string names). */
|
|
103
|
+
labels?: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Input for updating an existing merge request / pull request. */
|
|
107
|
+
export interface UpdatePullRequestInput {
|
|
108
|
+
title?: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
/** Set draft status. */
|
|
111
|
+
draft?: boolean;
|
|
112
|
+
/** Change the target branch. */
|
|
113
|
+
targetBranch?: string;
|
|
114
|
+
/** Usernames to assign as reviewers (replaces current set). */
|
|
115
|
+
reviewers?: string[];
|
|
116
|
+
/** Usernames to assign (replaces current set). */
|
|
117
|
+
assignees?: string[];
|
|
118
|
+
/** Labels (replaces current set). */
|
|
119
|
+
labels?: string[];
|
|
120
|
+
/** Set MR state: "close" or "reopen". */
|
|
121
|
+
stateEvent?: "close" | "reopen";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Branch protection rule (provider-agnostic). */
|
|
125
|
+
export interface BranchProtectionRule {
|
|
126
|
+
/** Branch name or pattern, e.g. "main" or "release/*". */
|
|
127
|
+
pattern: string;
|
|
128
|
+
/** Whether force-pushes are allowed. */
|
|
129
|
+
allowForcePush: boolean;
|
|
130
|
+
/** Whether branch deletions are allowed. */
|
|
131
|
+
allowDeletion: boolean;
|
|
132
|
+
/** Number of required approving reviews (0 = none required). */
|
|
133
|
+
requiredApprovals: number;
|
|
134
|
+
/** Whether the branch requires status checks to pass before merge. */
|
|
135
|
+
requireStatusChecks: boolean;
|
|
136
|
+
/** Raw provider-specific data for fields not covered above. */
|
|
137
|
+
raw?: Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
|
|
87
140
|
/** Snapshot payload sent when a client first connects. */
|
|
88
141
|
export interface PullRequestsSnapshot {
|
|
89
142
|
items: PullRequest[];
|