mcp-server-bitbucket 0.11.0
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/Dockerfile +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2976 -0
- package/docker-entrypoint.sh +6 -0
- package/package.json +57 -0
- package/smithery.yaml +47 -0
- package/src/client.ts +1102 -0
- package/src/index.ts +149 -0
- package/src/prompts.ts +208 -0
- package/src/resources.ts +160 -0
- package/src/settings.ts +63 -0
- package/src/tools/branches.ts +69 -0
- package/src/tools/commits.ts +186 -0
- package/src/tools/deployments.ts +100 -0
- package/src/tools/index.ts +112 -0
- package/src/tools/permissions.ts +205 -0
- package/src/tools/pipelines.ts +301 -0
- package/src/tools/projects.ts +63 -0
- package/src/tools/pull-requests.ts +321 -0
- package/src/tools/repositories.ts +208 -0
- package/src/tools/restrictions.ts +94 -0
- package/src/tools/source.ts +78 -0
- package/src/tools/tags.ts +88 -0
- package/src/tools/webhooks.ts +121 -0
- package/src/types.ts +369 -0
- package/src/utils.ts +83 -0
- package/tsconfig.json +25 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket API client for MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Provides all Bitbucket API operations needed by the MCP tools:
|
|
5
|
+
* - Repositories: get, create, delete, list, update
|
|
6
|
+
* - Pull Requests: create, get, list, merge, approve, decline, comments, diff
|
|
7
|
+
* - Pipelines: trigger, get, list, logs, stop
|
|
8
|
+
* - Branches: list, get
|
|
9
|
+
* - Commits: list, get, compare, statuses
|
|
10
|
+
* - Deployments: environments, deployment history
|
|
11
|
+
* - Webhooks: list, create, get, delete
|
|
12
|
+
* - Tags: list, create, delete
|
|
13
|
+
* - Branch Restrictions: list, create, delete
|
|
14
|
+
* - Source: file content, directory listing
|
|
15
|
+
* - Permissions: user and group permissions
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
19
|
+
import { getSettings } from './settings.js';
|
|
20
|
+
import { ensureUuidBraces, sleep } from './utils.js';
|
|
21
|
+
import type {
|
|
22
|
+
BitbucketRepository,
|
|
23
|
+
BitbucketBranch,
|
|
24
|
+
BitbucketCommit,
|
|
25
|
+
BitbucketPullRequest,
|
|
26
|
+
BitbucketPipeline,
|
|
27
|
+
BitbucketPipelineStep,
|
|
28
|
+
BitbucketPipelineVariable,
|
|
29
|
+
BitbucketEnvironment,
|
|
30
|
+
BitbucketDeployment,
|
|
31
|
+
BitbucketWebhook,
|
|
32
|
+
BitbucketTag,
|
|
33
|
+
BitbucketBranchRestriction,
|
|
34
|
+
BitbucketComment,
|
|
35
|
+
BitbucketCommitStatus,
|
|
36
|
+
BitbucketProject,
|
|
37
|
+
DirectoryEntry,
|
|
38
|
+
UserPermission,
|
|
39
|
+
GroupPermission,
|
|
40
|
+
PaginatedResponse,
|
|
41
|
+
TriggerPipelineOptions,
|
|
42
|
+
PipelineTriggerVariable,
|
|
43
|
+
} from './types.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Error class for Bitbucket API errors
|
|
47
|
+
*/
|
|
48
|
+
export class BitbucketError extends Error {
|
|
49
|
+
constructor(
|
|
50
|
+
message: string,
|
|
51
|
+
public statusCode?: number,
|
|
52
|
+
public method?: string,
|
|
53
|
+
public path?: string
|
|
54
|
+
) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'BitbucketError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bitbucket API client with connection pooling and retry logic
|
|
62
|
+
*/
|
|
63
|
+
export class BitbucketClient {
|
|
64
|
+
private static readonly BASE_URL = 'https://api.bitbucket.org/2.0';
|
|
65
|
+
private static readonly INITIAL_BACKOFF = 1000; // ms
|
|
66
|
+
|
|
67
|
+
public readonly workspace: string;
|
|
68
|
+
private readonly client: AxiosInstance;
|
|
69
|
+
private readonly maxRetries: number;
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
const settings = getSettings();
|
|
73
|
+
|
|
74
|
+
this.workspace = settings.bitbucketWorkspace;
|
|
75
|
+
this.maxRetries = settings.maxRetries;
|
|
76
|
+
|
|
77
|
+
this.client = axios.create({
|
|
78
|
+
baseURL: BitbucketClient.BASE_URL,
|
|
79
|
+
timeout: settings.apiTimeout * 1000,
|
|
80
|
+
auth: {
|
|
81
|
+
username: settings.bitbucketEmail,
|
|
82
|
+
password: settings.bitbucketApiToken,
|
|
83
|
+
},
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build repository endpoint path
|
|
92
|
+
*/
|
|
93
|
+
private repoPath(repoSlug: string, ...parts: string[]): string {
|
|
94
|
+
const base = `repositories/${this.workspace}/${repoSlug}`;
|
|
95
|
+
return parts.length > 0 ? `${base}/${parts.join('/')}` : base;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Make an API request with retry logic for rate limiting
|
|
100
|
+
*/
|
|
101
|
+
private async request<T>(
|
|
102
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
103
|
+
path: string,
|
|
104
|
+
data?: unknown,
|
|
105
|
+
params?: Record<string, unknown>
|
|
106
|
+
): Promise<T | null> {
|
|
107
|
+
let backoff = BitbucketClient.INITIAL_BACKOFF;
|
|
108
|
+
|
|
109
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
const response = await this.client.request<T>({
|
|
112
|
+
method,
|
|
113
|
+
url: path,
|
|
114
|
+
data,
|
|
115
|
+
params,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return response.data;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (axios.isAxiosError(error)) {
|
|
121
|
+
const axiosError = error as AxiosError;
|
|
122
|
+
|
|
123
|
+
// Handle 404 as null (not found)
|
|
124
|
+
if (axiosError.response?.status === 404) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle rate limiting (429)
|
|
129
|
+
if (axiosError.response?.status === 429) {
|
|
130
|
+
if (attempt < this.maxRetries) {
|
|
131
|
+
const retryAfter = axiosError.response.headers['retry-after'];
|
|
132
|
+
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : backoff;
|
|
133
|
+
await sleep(waitTime);
|
|
134
|
+
backoff *= 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw new BitbucketError(
|
|
138
|
+
`Rate limited after ${this.maxRetries} retries`,
|
|
139
|
+
429,
|
|
140
|
+
method,
|
|
141
|
+
path
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Other errors
|
|
146
|
+
const statusCode = axiosError.response?.status;
|
|
147
|
+
const errorText = JSON.stringify(axiosError.response?.data || axiosError.message).substring(0, 500);
|
|
148
|
+
throw new BitbucketError(
|
|
149
|
+
`API error ${statusCode}: ${errorText}`,
|
|
150
|
+
statusCode,
|
|
151
|
+
method,
|
|
152
|
+
path
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new BitbucketError(`Unexpected error in request`, undefined, method, path);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Make a request that returns plain text
|
|
164
|
+
*/
|
|
165
|
+
private async requestText(path: string): Promise<string | null> {
|
|
166
|
+
let backoff = BitbucketClient.INITIAL_BACKOFF;
|
|
167
|
+
|
|
168
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
169
|
+
try {
|
|
170
|
+
const response = await this.client.get(path, {
|
|
171
|
+
responseType: 'text',
|
|
172
|
+
});
|
|
173
|
+
return response.data;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (axios.isAxiosError(error)) {
|
|
176
|
+
if (error.response?.status === 404) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (error.response?.status === 429) {
|
|
180
|
+
if (attempt < this.maxRetries) {
|
|
181
|
+
const retryAfter = error.response.headers['retry-after'];
|
|
182
|
+
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : backoff;
|
|
183
|
+
await sleep(waitTime);
|
|
184
|
+
backoff *= 2;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw new BitbucketError(`Request failed: ${error.response?.status}`);
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Helper for paginated list endpoints
|
|
198
|
+
*/
|
|
199
|
+
private async paginatedList<T>(
|
|
200
|
+
endpoint: string,
|
|
201
|
+
options: { limit?: number; maxPage?: number } & Record<string, unknown> = {}
|
|
202
|
+
): Promise<T[]> {
|
|
203
|
+
const { limit = 50, maxPage = 100, ...extraParams } = options;
|
|
204
|
+
const params: Record<string, unknown> = {
|
|
205
|
+
pagelen: Math.min(limit, maxPage),
|
|
206
|
+
...extraParams,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Filter out undefined values
|
|
210
|
+
Object.keys(params).forEach(key => {
|
|
211
|
+
if (params[key] === undefined) {
|
|
212
|
+
delete params[key];
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = await this.request<PaginatedResponse<T>>('GET', endpoint, undefined, params);
|
|
217
|
+
return result?.values || [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ==================== REPOSITORIES ====================
|
|
221
|
+
|
|
222
|
+
async getRepository(repoSlug: string): Promise<BitbucketRepository | null> {
|
|
223
|
+
return this.request('GET', this.repoPath(repoSlug));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async createRepository(
|
|
227
|
+
repoSlug: string,
|
|
228
|
+
options: {
|
|
229
|
+
projectKey?: string;
|
|
230
|
+
isPrivate?: boolean;
|
|
231
|
+
description?: string;
|
|
232
|
+
} = {}
|
|
233
|
+
): Promise<BitbucketRepository> {
|
|
234
|
+
const payload: Record<string, unknown> = {
|
|
235
|
+
scm: 'git',
|
|
236
|
+
is_private: options.isPrivate ?? true,
|
|
237
|
+
};
|
|
238
|
+
if (options.projectKey) {
|
|
239
|
+
payload.project = { key: options.projectKey };
|
|
240
|
+
}
|
|
241
|
+
if (options.description) {
|
|
242
|
+
payload.description = options.description;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = await this.request<BitbucketRepository>('POST', this.repoPath(repoSlug), payload);
|
|
246
|
+
if (!result) {
|
|
247
|
+
throw new BitbucketError(`Failed to create repository: ${repoSlug}`);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async deleteRepository(repoSlug: string): Promise<void> {
|
|
253
|
+
await this.request('DELETE', this.repoPath(repoSlug));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async listRepositories(
|
|
257
|
+
options: {
|
|
258
|
+
projectKey?: string;
|
|
259
|
+
query?: string;
|
|
260
|
+
limit?: number;
|
|
261
|
+
} = {}
|
|
262
|
+
): Promise<BitbucketRepository[]> {
|
|
263
|
+
const params: Record<string, unknown> = {
|
|
264
|
+
pagelen: Math.min(options.limit || 50, 100),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const qParts: string[] = [];
|
|
268
|
+
if (options.projectKey) {
|
|
269
|
+
qParts.push(`project.key="${options.projectKey}"`);
|
|
270
|
+
}
|
|
271
|
+
if (options.query) {
|
|
272
|
+
qParts.push(options.query);
|
|
273
|
+
}
|
|
274
|
+
if (qParts.length > 0) {
|
|
275
|
+
params.q = qParts.join(' AND ');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await this.request<PaginatedResponse<BitbucketRepository>>(
|
|
279
|
+
'GET',
|
|
280
|
+
`repositories/${this.workspace}`,
|
|
281
|
+
undefined,
|
|
282
|
+
params
|
|
283
|
+
);
|
|
284
|
+
return result?.values || [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async updateRepository(
|
|
288
|
+
repoSlug: string,
|
|
289
|
+
options: {
|
|
290
|
+
projectKey?: string;
|
|
291
|
+
isPrivate?: boolean;
|
|
292
|
+
description?: string;
|
|
293
|
+
name?: string;
|
|
294
|
+
}
|
|
295
|
+
): Promise<BitbucketRepository> {
|
|
296
|
+
const payload: Record<string, unknown> = {};
|
|
297
|
+
if (options.projectKey !== undefined) {
|
|
298
|
+
payload.project = { key: options.projectKey };
|
|
299
|
+
}
|
|
300
|
+
if (options.isPrivate !== undefined) {
|
|
301
|
+
payload.is_private = options.isPrivate;
|
|
302
|
+
}
|
|
303
|
+
if (options.description !== undefined) {
|
|
304
|
+
payload.description = options.description;
|
|
305
|
+
}
|
|
306
|
+
if (options.name !== undefined) {
|
|
307
|
+
payload.name = options.name;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (Object.keys(payload).length === 0) {
|
|
311
|
+
throw new BitbucketError('No fields to update');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result = await this.request<BitbucketRepository>('PUT', this.repoPath(repoSlug), payload);
|
|
315
|
+
if (!result) {
|
|
316
|
+
throw new BitbucketError(`Failed to update repository: ${repoSlug}`);
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ==================== PULL REQUESTS ====================
|
|
322
|
+
|
|
323
|
+
async createPullRequest(
|
|
324
|
+
repoSlug: string,
|
|
325
|
+
options: {
|
|
326
|
+
title: string;
|
|
327
|
+
sourceBranch: string;
|
|
328
|
+
destinationBranch?: string;
|
|
329
|
+
description?: string;
|
|
330
|
+
closeSourceBranch?: boolean;
|
|
331
|
+
reviewers?: string[];
|
|
332
|
+
}
|
|
333
|
+
): Promise<BitbucketPullRequest> {
|
|
334
|
+
const payload: Record<string, unknown> = {
|
|
335
|
+
title: options.title,
|
|
336
|
+
source: { branch: { name: options.sourceBranch } },
|
|
337
|
+
destination: { branch: { name: options.destinationBranch || 'main' } },
|
|
338
|
+
close_source_branch: options.closeSourceBranch ?? true,
|
|
339
|
+
};
|
|
340
|
+
if (options.description) {
|
|
341
|
+
payload.description = options.description;
|
|
342
|
+
}
|
|
343
|
+
if (options.reviewers && options.reviewers.length > 0) {
|
|
344
|
+
payload.reviewers = options.reviewers.map(r =>
|
|
345
|
+
r.startsWith('{') ? { uuid: r } : { account_id: r }
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = await this.request<BitbucketPullRequest>(
|
|
350
|
+
'POST',
|
|
351
|
+
this.repoPath(repoSlug, 'pullrequests'),
|
|
352
|
+
payload
|
|
353
|
+
);
|
|
354
|
+
if (!result) {
|
|
355
|
+
throw new BitbucketError(`Failed to create PR: ${options.sourceBranch} -> ${options.destinationBranch || 'main'}`);
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getPullRequest(repoSlug: string, prId: number): Promise<BitbucketPullRequest | null> {
|
|
361
|
+
return this.request('GET', this.repoPath(repoSlug, 'pullrequests', String(prId)));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async listPullRequests(
|
|
365
|
+
repoSlug: string,
|
|
366
|
+
options: { state?: string; limit?: number } = {}
|
|
367
|
+
): Promise<BitbucketPullRequest[]> {
|
|
368
|
+
return this.paginatedList(this.repoPath(repoSlug, 'pullrequests'), {
|
|
369
|
+
limit: options.limit || 50,
|
|
370
|
+
maxPage: 50,
|
|
371
|
+
state: options.state || 'OPEN',
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async mergePullRequest(
|
|
376
|
+
repoSlug: string,
|
|
377
|
+
prId: number,
|
|
378
|
+
options: {
|
|
379
|
+
mergeStrategy?: string;
|
|
380
|
+
closeSourceBranch?: boolean;
|
|
381
|
+
message?: string;
|
|
382
|
+
} = {}
|
|
383
|
+
): Promise<BitbucketPullRequest> {
|
|
384
|
+
const payload: Record<string, unknown> = {
|
|
385
|
+
type: options.mergeStrategy || 'merge_commit',
|
|
386
|
+
close_source_branch: options.closeSourceBranch ?? true,
|
|
387
|
+
};
|
|
388
|
+
if (options.message) {
|
|
389
|
+
payload.message = options.message;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const result = await this.request<BitbucketPullRequest>(
|
|
393
|
+
'POST',
|
|
394
|
+
this.repoPath(repoSlug, 'pullrequests', String(prId), 'merge'),
|
|
395
|
+
payload
|
|
396
|
+
);
|
|
397
|
+
if (!result) {
|
|
398
|
+
throw new BitbucketError(`Failed to merge PR #${prId}`);
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async listPrComments(
|
|
404
|
+
repoSlug: string,
|
|
405
|
+
prId: number,
|
|
406
|
+
options: { limit?: number } = {}
|
|
407
|
+
): Promise<BitbucketComment[]> {
|
|
408
|
+
return this.paginatedList(
|
|
409
|
+
this.repoPath(repoSlug, 'pullrequests', String(prId), 'comments'),
|
|
410
|
+
{ limit: options.limit || 50 }
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async addPrComment(
|
|
415
|
+
repoSlug: string,
|
|
416
|
+
prId: number,
|
|
417
|
+
content: string,
|
|
418
|
+
inline?: { path: string; to: number }
|
|
419
|
+
): Promise<BitbucketComment> {
|
|
420
|
+
const payload: Record<string, unknown> = {
|
|
421
|
+
content: { raw: content },
|
|
422
|
+
};
|
|
423
|
+
if (inline) {
|
|
424
|
+
payload.inline = inline;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const result = await this.request<BitbucketComment>(
|
|
428
|
+
'POST',
|
|
429
|
+
this.repoPath(repoSlug, 'pullrequests', String(prId), 'comments'),
|
|
430
|
+
payload
|
|
431
|
+
);
|
|
432
|
+
if (!result) {
|
|
433
|
+
throw new BitbucketError(`Failed to add comment to PR #${prId}`);
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async approvePr(repoSlug: string, prId: number): Promise<Record<string, unknown>> {
|
|
439
|
+
const result = await this.request<Record<string, unknown>>(
|
|
440
|
+
'POST',
|
|
441
|
+
this.repoPath(repoSlug, 'pullrequests', String(prId), 'approve')
|
|
442
|
+
);
|
|
443
|
+
if (!result) {
|
|
444
|
+
throw new BitbucketError(`Failed to approve PR #${prId}`);
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async unapprovePr(repoSlug: string, prId: number): Promise<void> {
|
|
450
|
+
await this.request('DELETE', this.repoPath(repoSlug, 'pullrequests', String(prId), 'approve'));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async requestChangesPr(repoSlug: string, prId: number): Promise<Record<string, unknown>> {
|
|
454
|
+
const result = await this.request<Record<string, unknown>>(
|
|
455
|
+
'POST',
|
|
456
|
+
this.repoPath(repoSlug, 'pullrequests', String(prId), 'request-changes')
|
|
457
|
+
);
|
|
458
|
+
if (!result) {
|
|
459
|
+
throw new BitbucketError(`Failed to request changes on PR #${prId}`);
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async declinePr(repoSlug: string, prId: number): Promise<BitbucketPullRequest> {
|
|
465
|
+
const result = await this.request<BitbucketPullRequest>(
|
|
466
|
+
'POST',
|
|
467
|
+
this.repoPath(repoSlug, 'pullrequests', String(prId), 'decline')
|
|
468
|
+
);
|
|
469
|
+
if (!result) {
|
|
470
|
+
throw new BitbucketError(`Failed to decline PR #${prId}`);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async getPrDiff(repoSlug: string, prId: number): Promise<string> {
|
|
476
|
+
return (await this.requestText(this.repoPath(repoSlug, 'pullrequests', String(prId), 'diff'))) || '';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ==================== PIPELINES ====================
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Build the pipeline target object based on options.
|
|
483
|
+
* Supports branch triggers, commit triggers, and custom pipelines.
|
|
484
|
+
*/
|
|
485
|
+
private buildPipelineTarget(options: TriggerPipelineOptions): Record<string, unknown> {
|
|
486
|
+
// Validate mutual exclusivity of branch and commit
|
|
487
|
+
if (options.branch && options.commit) {
|
|
488
|
+
throw new BitbucketError('Cannot specify both branch and commit - they are mutually exclusive');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Build target based on trigger type
|
|
492
|
+
if (options.commit) {
|
|
493
|
+
// Commit-based trigger
|
|
494
|
+
const target: Record<string, unknown> = {
|
|
495
|
+
type: 'pipeline_commit_target',
|
|
496
|
+
commit: { hash: options.commit },
|
|
497
|
+
};
|
|
498
|
+
// Add selector for custom pipeline if specified
|
|
499
|
+
if (options.customPipeline) {
|
|
500
|
+
target.selector = {
|
|
501
|
+
type: 'custom',
|
|
502
|
+
pattern: options.customPipeline,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return target;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Branch-based trigger (default)
|
|
509
|
+
const target: Record<string, unknown> = {
|
|
510
|
+
type: 'pipeline_ref_target',
|
|
511
|
+
ref_type: 'branch',
|
|
512
|
+
ref_name: options.branch || 'main',
|
|
513
|
+
};
|
|
514
|
+
// Add selector for custom pipeline if specified
|
|
515
|
+
if (options.customPipeline) {
|
|
516
|
+
target.selector = {
|
|
517
|
+
type: 'custom',
|
|
518
|
+
pattern: options.customPipeline,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return target;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Normalize pipeline variables to the array format expected by the API.
|
|
526
|
+
* Supports both array format (with secured flag) and simple object format.
|
|
527
|
+
*/
|
|
528
|
+
private normalizePipelineVariables(
|
|
529
|
+
variables?: PipelineTriggerVariable[] | Record<string, string>
|
|
530
|
+
): { key: string; value: string; secured?: boolean }[] | undefined {
|
|
531
|
+
if (!variables) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// If already an array, return as-is (with secured flag preserved)
|
|
536
|
+
if (Array.isArray(variables)) {
|
|
537
|
+
return variables.map(v => ({
|
|
538
|
+
key: v.key,
|
|
539
|
+
value: v.value,
|
|
540
|
+
...(v.secured !== undefined && { secured: v.secured }),
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Convert object format to array format (without secured flag)
|
|
545
|
+
return Object.entries(variables).map(([key, value]) => ({
|
|
546
|
+
key,
|
|
547
|
+
value,
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async triggerPipeline(
|
|
552
|
+
repoSlug: string,
|
|
553
|
+
options: TriggerPipelineOptions = {}
|
|
554
|
+
): Promise<BitbucketPipeline> {
|
|
555
|
+
const payload: Record<string, unknown> = {
|
|
556
|
+
target: this.buildPipelineTarget(options),
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const normalizedVariables = this.normalizePipelineVariables(options.variables);
|
|
560
|
+
if (normalizedVariables && normalizedVariables.length > 0) {
|
|
561
|
+
payload.variables = normalizedVariables;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const result = await this.request<BitbucketPipeline>(
|
|
565
|
+
'POST',
|
|
566
|
+
`${this.repoPath(repoSlug, 'pipelines')}/`,
|
|
567
|
+
payload
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const targetDesc = options.commit
|
|
571
|
+
? `commit ${options.commit}`
|
|
572
|
+
: (options.branch || 'main');
|
|
573
|
+
const pipelineDesc = options.customPipeline
|
|
574
|
+
? `custom:${options.customPipeline}`
|
|
575
|
+
: 'default';
|
|
576
|
+
|
|
577
|
+
if (!result) {
|
|
578
|
+
throw new BitbucketError(`Failed to trigger ${pipelineDesc} pipeline on ${targetDesc}`);
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async getPipeline(repoSlug: string, pipelineUuid: string): Promise<BitbucketPipeline | null> {
|
|
584
|
+
return this.request('GET', this.repoPath(repoSlug, 'pipelines', ensureUuidBraces(pipelineUuid)));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async listPipelines(
|
|
588
|
+
repoSlug: string,
|
|
589
|
+
options: { limit?: number } = {}
|
|
590
|
+
): Promise<BitbucketPipeline[]> {
|
|
591
|
+
return this.paginatedList(`${this.repoPath(repoSlug, 'pipelines')}/`, {
|
|
592
|
+
limit: options.limit || 10,
|
|
593
|
+
sort: '-created_on',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async getPipelineSteps(
|
|
598
|
+
repoSlug: string,
|
|
599
|
+
pipelineUuid: string
|
|
600
|
+
): Promise<BitbucketPipelineStep[]> {
|
|
601
|
+
return this.paginatedList(
|
|
602
|
+
`${this.repoPath(repoSlug, 'pipelines', ensureUuidBraces(pipelineUuid), 'steps')}/`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async getPipelineLogs(
|
|
607
|
+
repoSlug: string,
|
|
608
|
+
pipelineUuid: string,
|
|
609
|
+
stepUuid: string
|
|
610
|
+
): Promise<string> {
|
|
611
|
+
const path = this.repoPath(
|
|
612
|
+
repoSlug,
|
|
613
|
+
'pipelines',
|
|
614
|
+
ensureUuidBraces(pipelineUuid),
|
|
615
|
+
'steps',
|
|
616
|
+
ensureUuidBraces(stepUuid),
|
|
617
|
+
'log'
|
|
618
|
+
);
|
|
619
|
+
return (await this.requestText(path)) || '';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async stopPipeline(repoSlug: string, pipelineUuid: string): Promise<BitbucketPipeline> {
|
|
623
|
+
await this.request(
|
|
624
|
+
'POST',
|
|
625
|
+
this.repoPath(repoSlug, 'pipelines', ensureUuidBraces(pipelineUuid), 'stopPipeline')
|
|
626
|
+
);
|
|
627
|
+
const result = await this.getPipeline(repoSlug, pipelineUuid);
|
|
628
|
+
return result || { uuid: pipelineUuid, state: { name: 'STOPPED' } };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ==================== PIPELINE VARIABLES ====================
|
|
632
|
+
|
|
633
|
+
async listPipelineVariables(
|
|
634
|
+
repoSlug: string,
|
|
635
|
+
options: { limit?: number } = {}
|
|
636
|
+
): Promise<BitbucketPipelineVariable[]> {
|
|
637
|
+
return this.paginatedList(
|
|
638
|
+
this.repoPath(repoSlug, 'pipelines_config', 'variables'),
|
|
639
|
+
{ limit: options.limit || 50 }
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async getPipelineVariable(
|
|
644
|
+
repoSlug: string,
|
|
645
|
+
variableUuid: string
|
|
646
|
+
): Promise<BitbucketPipelineVariable | null> {
|
|
647
|
+
return this.request(
|
|
648
|
+
'GET',
|
|
649
|
+
this.repoPath(repoSlug, 'pipelines_config', 'variables', ensureUuidBraces(variableUuid))
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async createPipelineVariable(
|
|
654
|
+
repoSlug: string,
|
|
655
|
+
key: string,
|
|
656
|
+
value: string,
|
|
657
|
+
secured: boolean = false
|
|
658
|
+
): Promise<BitbucketPipelineVariable> {
|
|
659
|
+
const result = await this.request<BitbucketPipelineVariable>(
|
|
660
|
+
'POST',
|
|
661
|
+
`${this.repoPath(repoSlug, 'pipelines_config', 'variables')}/`,
|
|
662
|
+
{ key, value, secured }
|
|
663
|
+
);
|
|
664
|
+
if (!result) {
|
|
665
|
+
throw new BitbucketError('Failed to create pipeline variable');
|
|
666
|
+
}
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async updatePipelineVariable(
|
|
671
|
+
repoSlug: string,
|
|
672
|
+
variableUuid: string,
|
|
673
|
+
value: string
|
|
674
|
+
): Promise<BitbucketPipelineVariable> {
|
|
675
|
+
const result = await this.request<BitbucketPipelineVariable>(
|
|
676
|
+
'PUT',
|
|
677
|
+
this.repoPath(repoSlug, 'pipelines_config', 'variables', ensureUuidBraces(variableUuid)),
|
|
678
|
+
{ value }
|
|
679
|
+
);
|
|
680
|
+
if (!result) {
|
|
681
|
+
throw new BitbucketError('Failed to update pipeline variable');
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async deletePipelineVariable(repoSlug: string, variableUuid: string): Promise<void> {
|
|
687
|
+
await this.request(
|
|
688
|
+
'DELETE',
|
|
689
|
+
this.repoPath(repoSlug, 'pipelines_config', 'variables', ensureUuidBraces(variableUuid))
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ==================== BRANCHES ====================
|
|
694
|
+
|
|
695
|
+
async listBranches(
|
|
696
|
+
repoSlug: string,
|
|
697
|
+
options: { limit?: number } = {}
|
|
698
|
+
): Promise<BitbucketBranch[]> {
|
|
699
|
+
return this.paginatedList(this.repoPath(repoSlug, 'refs', 'branches'), {
|
|
700
|
+
limit: options.limit || 50,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async getBranch(repoSlug: string, branchName: string): Promise<BitbucketBranch | null> {
|
|
705
|
+
return this.request('GET', this.repoPath(repoSlug, 'refs', 'branches', branchName));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ==================== COMMITS ====================
|
|
709
|
+
|
|
710
|
+
async listCommits(
|
|
711
|
+
repoSlug: string,
|
|
712
|
+
options: { branch?: string; path?: string; limit?: number } = {}
|
|
713
|
+
): Promise<BitbucketCommit[]> {
|
|
714
|
+
return this.paginatedList(this.repoPath(repoSlug, 'commits'), {
|
|
715
|
+
limit: options.limit || 20,
|
|
716
|
+
include: options.branch,
|
|
717
|
+
path: options.path,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async getCommit(repoSlug: string, commit: string): Promise<BitbucketCommit | null> {
|
|
722
|
+
return this.request('GET', this.repoPath(repoSlug, 'commit', commit));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async compareCommits(
|
|
726
|
+
repoSlug: string,
|
|
727
|
+
base: string,
|
|
728
|
+
head: string
|
|
729
|
+
): Promise<Record<string, unknown> | null> {
|
|
730
|
+
return this.request('GET', this.repoPath(repoSlug, 'diffstat', `${base}..${head}`));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async getCommitStatuses(
|
|
734
|
+
repoSlug: string,
|
|
735
|
+
commit: string,
|
|
736
|
+
options: { limit?: number } = {}
|
|
737
|
+
): Promise<BitbucketCommitStatus[]> {
|
|
738
|
+
return this.paginatedList(this.repoPath(repoSlug, 'commit', commit, 'statuses'), {
|
|
739
|
+
limit: options.limit || 20,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async createCommitStatus(
|
|
744
|
+
repoSlug: string,
|
|
745
|
+
commit: string,
|
|
746
|
+
options: {
|
|
747
|
+
state: string;
|
|
748
|
+
key: string;
|
|
749
|
+
url?: string;
|
|
750
|
+
name?: string;
|
|
751
|
+
description?: string;
|
|
752
|
+
}
|
|
753
|
+
): Promise<BitbucketCommitStatus> {
|
|
754
|
+
const payload: Record<string, unknown> = {
|
|
755
|
+
state: options.state,
|
|
756
|
+
key: options.key,
|
|
757
|
+
};
|
|
758
|
+
if (options.url) payload.url = options.url;
|
|
759
|
+
if (options.name) payload.name = options.name;
|
|
760
|
+
if (options.description) payload.description = options.description;
|
|
761
|
+
|
|
762
|
+
const result = await this.request<BitbucketCommitStatus>(
|
|
763
|
+
'POST',
|
|
764
|
+
this.repoPath(repoSlug, 'commit', commit, 'statuses', 'build'),
|
|
765
|
+
payload
|
|
766
|
+
);
|
|
767
|
+
if (!result) {
|
|
768
|
+
throw new BitbucketError(`Failed to create status for commit ${commit}`);
|
|
769
|
+
}
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ==================== PROJECTS ====================
|
|
774
|
+
|
|
775
|
+
async listProjects(options: { limit?: number } = {}): Promise<BitbucketProject[]> {
|
|
776
|
+
return this.paginatedList(`workspaces/${this.workspace}/projects`, {
|
|
777
|
+
limit: options.limit || 50,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async getProject(projectKey: string): Promise<BitbucketProject | null> {
|
|
782
|
+
return this.request('GET', `workspaces/${this.workspace}/projects/${projectKey}`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ==================== DEPLOYMENTS ====================
|
|
786
|
+
|
|
787
|
+
async listEnvironments(
|
|
788
|
+
repoSlug: string,
|
|
789
|
+
options: { limit?: number } = {}
|
|
790
|
+
): Promise<BitbucketEnvironment[]> {
|
|
791
|
+
return this.paginatedList(this.repoPath(repoSlug, 'environments'), {
|
|
792
|
+
limit: options.limit || 20,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async getEnvironment(
|
|
797
|
+
repoSlug: string,
|
|
798
|
+
environmentUuid: string
|
|
799
|
+
): Promise<BitbucketEnvironment | null> {
|
|
800
|
+
return this.request(
|
|
801
|
+
'GET',
|
|
802
|
+
this.repoPath(repoSlug, 'environments', ensureUuidBraces(environmentUuid))
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async listDeploymentHistory(
|
|
807
|
+
repoSlug: string,
|
|
808
|
+
environmentUuid: string,
|
|
809
|
+
options: { limit?: number } = {}
|
|
810
|
+
): Promise<BitbucketDeployment[]> {
|
|
811
|
+
return this.paginatedList(this.repoPath(repoSlug, 'deployments'), {
|
|
812
|
+
limit: options.limit || 20,
|
|
813
|
+
environment: ensureUuidBraces(environmentUuid),
|
|
814
|
+
sort: '-state.started_on',
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ==================== WEBHOOKS ====================
|
|
819
|
+
|
|
820
|
+
async listWebhooks(
|
|
821
|
+
repoSlug: string,
|
|
822
|
+
options: { limit?: number } = {}
|
|
823
|
+
): Promise<BitbucketWebhook[]> {
|
|
824
|
+
return this.paginatedList(this.repoPath(repoSlug, 'hooks'), {
|
|
825
|
+
limit: options.limit || 50,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async createWebhook(
|
|
830
|
+
repoSlug: string,
|
|
831
|
+
options: {
|
|
832
|
+
url: string;
|
|
833
|
+
events: string[];
|
|
834
|
+
description?: string;
|
|
835
|
+
active?: boolean;
|
|
836
|
+
}
|
|
837
|
+
): Promise<BitbucketWebhook> {
|
|
838
|
+
const payload: Record<string, unknown> = {
|
|
839
|
+
url: options.url,
|
|
840
|
+
events: options.events,
|
|
841
|
+
active: options.active ?? true,
|
|
842
|
+
};
|
|
843
|
+
if (options.description) {
|
|
844
|
+
payload.description = options.description;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const result = await this.request<BitbucketWebhook>(
|
|
848
|
+
'POST',
|
|
849
|
+
this.repoPath(repoSlug, 'hooks'),
|
|
850
|
+
payload
|
|
851
|
+
);
|
|
852
|
+
if (!result) {
|
|
853
|
+
throw new BitbucketError('Failed to create webhook');
|
|
854
|
+
}
|
|
855
|
+
return result;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async getWebhook(repoSlug: string, webhookUid: string): Promise<BitbucketWebhook | null> {
|
|
859
|
+
return this.request('GET', this.repoPath(repoSlug, 'hooks', ensureUuidBraces(webhookUid)));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async deleteWebhook(repoSlug: string, webhookUid: string): Promise<void> {
|
|
863
|
+
await this.request('DELETE', this.repoPath(repoSlug, 'hooks', ensureUuidBraces(webhookUid)));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ==================== TAGS ====================
|
|
867
|
+
|
|
868
|
+
async listTags(repoSlug: string, options: { limit?: number } = {}): Promise<BitbucketTag[]> {
|
|
869
|
+
return this.paginatedList(this.repoPath(repoSlug, 'refs', 'tags'), {
|
|
870
|
+
limit: options.limit || 50,
|
|
871
|
+
sort: '-target.date',
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async createTag(
|
|
876
|
+
repoSlug: string,
|
|
877
|
+
name: string,
|
|
878
|
+
target: string,
|
|
879
|
+
message?: string
|
|
880
|
+
): Promise<BitbucketTag> {
|
|
881
|
+
const payload: Record<string, unknown> = {
|
|
882
|
+
name,
|
|
883
|
+
target: { hash: target },
|
|
884
|
+
};
|
|
885
|
+
if (message) {
|
|
886
|
+
payload.message = message;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const result = await this.request<BitbucketTag>(
|
|
890
|
+
'POST',
|
|
891
|
+
this.repoPath(repoSlug, 'refs', 'tags'),
|
|
892
|
+
payload
|
|
893
|
+
);
|
|
894
|
+
if (!result) {
|
|
895
|
+
throw new BitbucketError(`Failed to create tag ${name}`);
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async deleteTag(repoSlug: string, tagName: string): Promise<void> {
|
|
901
|
+
await this.request('DELETE', this.repoPath(repoSlug, 'refs', 'tags', tagName));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ==================== BRANCH RESTRICTIONS ====================
|
|
905
|
+
|
|
906
|
+
async listBranchRestrictions(
|
|
907
|
+
repoSlug: string,
|
|
908
|
+
options: { limit?: number } = {}
|
|
909
|
+
): Promise<BitbucketBranchRestriction[]> {
|
|
910
|
+
return this.paginatedList(this.repoPath(repoSlug, 'branch-restrictions'), {
|
|
911
|
+
limit: options.limit || 50,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async createBranchRestriction(
|
|
916
|
+
repoSlug: string,
|
|
917
|
+
options: {
|
|
918
|
+
kind: string;
|
|
919
|
+
pattern?: string;
|
|
920
|
+
branchMatchKind?: string;
|
|
921
|
+
branchType?: string;
|
|
922
|
+
value?: number;
|
|
923
|
+
}
|
|
924
|
+
): Promise<BitbucketBranchRestriction> {
|
|
925
|
+
const payload: Record<string, unknown> = {
|
|
926
|
+
kind: options.kind,
|
|
927
|
+
branch_match_kind: options.branchMatchKind || 'glob',
|
|
928
|
+
};
|
|
929
|
+
if (options.branchMatchKind === 'glob' && options.pattern) {
|
|
930
|
+
payload.pattern = options.pattern;
|
|
931
|
+
}
|
|
932
|
+
if (options.branchMatchKind === 'branching_model' && options.branchType) {
|
|
933
|
+
payload.branch_type = options.branchType;
|
|
934
|
+
}
|
|
935
|
+
if (options.value !== undefined) {
|
|
936
|
+
payload.value = options.value;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const result = await this.request<BitbucketBranchRestriction>(
|
|
940
|
+
'POST',
|
|
941
|
+
this.repoPath(repoSlug, 'branch-restrictions'),
|
|
942
|
+
payload
|
|
943
|
+
);
|
|
944
|
+
if (!result) {
|
|
945
|
+
throw new BitbucketError(`Failed to create branch restriction ${options.kind}`);
|
|
946
|
+
}
|
|
947
|
+
return result;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async deleteBranchRestriction(repoSlug: string, restrictionId: number): Promise<void> {
|
|
951
|
+
await this.request(
|
|
952
|
+
'DELETE',
|
|
953
|
+
this.repoPath(repoSlug, 'branch-restrictions', String(restrictionId))
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// ==================== SOURCE ====================
|
|
958
|
+
|
|
959
|
+
async getFileContent(
|
|
960
|
+
repoSlug: string,
|
|
961
|
+
path: string,
|
|
962
|
+
ref: string = 'main'
|
|
963
|
+
): Promise<string | null> {
|
|
964
|
+
return this.requestText(this.repoPath(repoSlug, 'src', ref, path));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async listDirectory(
|
|
968
|
+
repoSlug: string,
|
|
969
|
+
path: string = '',
|
|
970
|
+
options: { ref?: string; limit?: number } = {}
|
|
971
|
+
): Promise<DirectoryEntry[]> {
|
|
972
|
+
const endpoint = path
|
|
973
|
+
? this.repoPath(repoSlug, 'src', options.ref || 'main', path)
|
|
974
|
+
: this.repoPath(repoSlug, 'src', options.ref || 'main');
|
|
975
|
+
return this.paginatedList(endpoint, { limit: options.limit || 100 });
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ==================== PERMISSIONS ====================
|
|
979
|
+
|
|
980
|
+
async listUserPermissions(
|
|
981
|
+
repoSlug: string,
|
|
982
|
+
options: { limit?: number } = {}
|
|
983
|
+
): Promise<UserPermission[]> {
|
|
984
|
+
return this.paginatedList(this.repoPath(repoSlug, 'permissions-config', 'users'), {
|
|
985
|
+
limit: options.limit || 50,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async getUserPermission(
|
|
990
|
+
repoSlug: string,
|
|
991
|
+
selectedUser: string
|
|
992
|
+
): Promise<UserPermission | null> {
|
|
993
|
+
return this.request(
|
|
994
|
+
'GET',
|
|
995
|
+
this.repoPath(repoSlug, 'permissions-config', 'users', selectedUser)
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async updateUserPermission(
|
|
1000
|
+
repoSlug: string,
|
|
1001
|
+
selectedUser: string,
|
|
1002
|
+
permission: string
|
|
1003
|
+
): Promise<UserPermission> {
|
|
1004
|
+
const result = await this.request<UserPermission>(
|
|
1005
|
+
'PUT',
|
|
1006
|
+
this.repoPath(repoSlug, 'permissions-config', 'users', selectedUser),
|
|
1007
|
+
{ permission }
|
|
1008
|
+
);
|
|
1009
|
+
if (!result) {
|
|
1010
|
+
throw new BitbucketError(`Failed to update permission for user ${selectedUser}`);
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async deleteUserPermission(repoSlug: string, selectedUser: string): Promise<void> {
|
|
1016
|
+
await this.request(
|
|
1017
|
+
'DELETE',
|
|
1018
|
+
this.repoPath(repoSlug, 'permissions-config', 'users', selectedUser)
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async listGroupPermissions(
|
|
1023
|
+
repoSlug: string,
|
|
1024
|
+
options: { limit?: number } = {}
|
|
1025
|
+
): Promise<GroupPermission[]> {
|
|
1026
|
+
return this.paginatedList(this.repoPath(repoSlug, 'permissions-config', 'groups'), {
|
|
1027
|
+
limit: options.limit || 50,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async getGroupPermission(
|
|
1032
|
+
repoSlug: string,
|
|
1033
|
+
groupSlug: string
|
|
1034
|
+
): Promise<GroupPermission | null> {
|
|
1035
|
+
return this.request(
|
|
1036
|
+
'GET',
|
|
1037
|
+
this.repoPath(repoSlug, 'permissions-config', 'groups', groupSlug)
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async updateGroupPermission(
|
|
1042
|
+
repoSlug: string,
|
|
1043
|
+
groupSlug: string,
|
|
1044
|
+
permission: string
|
|
1045
|
+
): Promise<GroupPermission> {
|
|
1046
|
+
const result = await this.request<GroupPermission>(
|
|
1047
|
+
'PUT',
|
|
1048
|
+
this.repoPath(repoSlug, 'permissions-config', 'groups', groupSlug),
|
|
1049
|
+
{ permission }
|
|
1050
|
+
);
|
|
1051
|
+
if (!result) {
|
|
1052
|
+
throw new BitbucketError(`Failed to update permission for group ${groupSlug}`);
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async deleteGroupPermission(repoSlug: string, groupSlug: string): Promise<void> {
|
|
1058
|
+
await this.request(
|
|
1059
|
+
'DELETE',
|
|
1060
|
+
this.repoPath(repoSlug, 'permissions-config', 'groups', groupSlug)
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ==================== UTILITIES ====================
|
|
1065
|
+
|
|
1066
|
+
extractPrUrl(pr: BitbucketPullRequest): string {
|
|
1067
|
+
return pr.links?.html?.href || '';
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
extractCloneUrls(repo: BitbucketRepository): Record<string, string> {
|
|
1071
|
+
const urls: Record<string, string> = {};
|
|
1072
|
+
for (const link of repo.links?.clone || []) {
|
|
1073
|
+
const name = (link.name || '').toLowerCase();
|
|
1074
|
+
if (name === 'https' || name === 'ssh') {
|
|
1075
|
+
urls[name] = link.href || '';
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
urls.html = repo.links?.html?.href || '';
|
|
1079
|
+
return urls;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Singleton instance
|
|
1084
|
+
let clientInstance: BitbucketClient | null = null;
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Get or create the BitbucketClient singleton
|
|
1088
|
+
*/
|
|
1089
|
+
export function getClient(): BitbucketClient {
|
|
1090
|
+
if (!clientInstance) {
|
|
1091
|
+
clientInstance = new BitbucketClient();
|
|
1092
|
+
}
|
|
1093
|
+
return clientInstance;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Reset the client singleton (useful for testing)
|
|
1098
|
+
*/
|
|
1099
|
+
export function resetClient(): void {
|
|
1100
|
+
clientInstance = null;
|
|
1101
|
+
}
|
|
1102
|
+
|