@zhanglc77/bitbucket-mcp-server 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +280 -0
- package/LICENSE +21 -0
- package/README.md +1090 -0
- package/build/handlers/branch-handlers.d.ts +55 -0
- package/build/handlers/branch-handlers.d.ts.map +1 -0
- package/build/handlers/branch-handlers.js +522 -0
- package/build/handlers/branch-handlers.js.map +1 -0
- package/build/handlers/file-handlers.d.ts +33 -0
- package/build/handlers/file-handlers.d.ts.map +1 -0
- package/build/handlers/file-handlers.js +308 -0
- package/build/handlers/file-handlers.js.map +1 -0
- package/build/handlers/pull-request-handlers.d.ts +101 -0
- package/build/handlers/pull-request-handlers.d.ts.map +1 -0
- package/build/handlers/pull-request-handlers.js +955 -0
- package/build/handlers/pull-request-handlers.js.map +1 -0
- package/build/handlers/review-handlers.d.ts +67 -0
- package/build/handlers/review-handlers.d.ts.map +1 -0
- package/build/handlers/review-handlers.js +252 -0
- package/build/handlers/review-handlers.js.map +1 -0
- package/build/handlers/search-handlers.d.ts +20 -0
- package/build/handlers/search-handlers.d.ts.map +1 -0
- package/build/handlers/search-handlers.js +151 -0
- package/build/handlers/search-handlers.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +120 -0
- package/build/index.js.map +1 -0
- package/build/tools/definitions.d.ts +1191 -0
- package/build/tools/definitions.d.ts.map +1 -0
- package/build/tools/definitions.js +655 -0
- package/build/tools/definitions.js.map +1 -0
- package/build/types/bitbucket.d.ts +483 -0
- package/build/types/bitbucket.d.ts.map +1 -0
- package/build/types/bitbucket.js +2 -0
- package/build/types/bitbucket.js.map +1 -0
- package/build/types/guards.d.ts +140 -0
- package/build/types/guards.d.ts.map +1 -0
- package/build/types/guards.js +140 -0
- package/build/types/guards.js.map +1 -0
- package/build/utils/api-client.d.ts +22 -0
- package/build/utils/api-client.d.ts.map +1 -0
- package/build/utils/api-client.js +111 -0
- package/build/utils/api-client.js.map +1 -0
- package/build/utils/diff-parser.d.ts +42 -0
- package/build/utils/diff-parser.d.ts.map +1 -0
- package/build/utils/diff-parser.js +165 -0
- package/build/utils/diff-parser.js.map +1 -0
- package/build/utils/formatters.d.ts +8 -0
- package/build/utils/formatters.d.ts.map +1 -0
- package/build/utils/formatters.js +207 -0
- package/build/utils/formatters.js.map +1 -0
- package/build/utils/suggestion-formatter.d.ts +6 -0
- package/build/utils/suggestion-formatter.d.ts.map +1 -0
- package/build/utils/suggestion-formatter.js +17 -0
- package/build/utils/suggestion-formatter.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { formatServerResponse, formatCloudResponse, formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
|
|
3
|
+
import { formatSuggestionComment } from '../utils/suggestion-formatter.js';
|
|
4
|
+
import { DiffParser } from '../utils/diff-parser.js';
|
|
5
|
+
import { isGetPullRequestArgs, isListPullRequestsArgs, isCreatePullRequestArgs, isUpdatePullRequestArgs, isAddCommentArgs, isMergePullRequestArgs, isListPrCommitsArgs } from '../types/guards.js';
|
|
6
|
+
export class PullRequestHandlers {
|
|
7
|
+
apiClient;
|
|
8
|
+
baseUrl;
|
|
9
|
+
username;
|
|
10
|
+
constructor(apiClient, baseUrl, username) {
|
|
11
|
+
this.apiClient = apiClient;
|
|
12
|
+
this.baseUrl = baseUrl;
|
|
13
|
+
this.username = username;
|
|
14
|
+
}
|
|
15
|
+
async getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath, contextLines = 3) {
|
|
16
|
+
let apiPath;
|
|
17
|
+
let config = {};
|
|
18
|
+
if (this.apiClient.getIsServer()) {
|
|
19
|
+
// Bitbucket Server API
|
|
20
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/diff`;
|
|
21
|
+
config.params = { contextLines };
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Bitbucket Cloud API
|
|
25
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diff`;
|
|
26
|
+
config.params = { context: contextLines };
|
|
27
|
+
}
|
|
28
|
+
config.headers = { 'Accept': 'text/plain' };
|
|
29
|
+
const rawDiff = await this.apiClient.makeRequest('get', apiPath, undefined, config);
|
|
30
|
+
const diffParser = new DiffParser();
|
|
31
|
+
const sections = diffParser.parseDiffIntoSections(rawDiff);
|
|
32
|
+
const filterOptions = {
|
|
33
|
+
filePath: filePath
|
|
34
|
+
};
|
|
35
|
+
const filteredResult = diffParser.filterSections(sections, filterOptions);
|
|
36
|
+
const filteredDiff = diffParser.reconstructDiff(filteredResult.sections);
|
|
37
|
+
return filteredDiff;
|
|
38
|
+
}
|
|
39
|
+
async handleGetPullRequest(args) {
|
|
40
|
+
if (!isGetPullRequestArgs(args)) {
|
|
41
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for get_pull_request');
|
|
42
|
+
}
|
|
43
|
+
const { workspace, repository, pull_request_id } = args;
|
|
44
|
+
try {
|
|
45
|
+
const apiPath = this.apiClient.getIsServer()
|
|
46
|
+
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
|
|
47
|
+
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
|
|
48
|
+
const pr = await this.apiClient.makeRequest('get', apiPath);
|
|
49
|
+
let mergeInfo = {};
|
|
50
|
+
if (this.apiClient.getIsServer() && pr.state === 'MERGED') {
|
|
51
|
+
try {
|
|
52
|
+
const activitiesPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/activities`;
|
|
53
|
+
const activitiesResponse = await this.apiClient.makeRequest('get', activitiesPath, undefined, {
|
|
54
|
+
params: { limit: 100 }
|
|
55
|
+
});
|
|
56
|
+
const activities = activitiesResponse.values || [];
|
|
57
|
+
const mergeActivity = activities.find((a) => a.action === 'MERGED');
|
|
58
|
+
if (mergeActivity) {
|
|
59
|
+
mergeInfo.mergeCommitHash = mergeActivity.commit?.id || null;
|
|
60
|
+
mergeInfo.mergedBy = mergeActivity.user?.displayName || null;
|
|
61
|
+
mergeInfo.mergedAt = new Date(mergeActivity.createdDate).toISOString();
|
|
62
|
+
if (mergeActivity.commit?.id) {
|
|
63
|
+
try {
|
|
64
|
+
const commitPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits/${mergeActivity.commit.id}`;
|
|
65
|
+
const commitResponse = await this.apiClient.makeRequest('get', commitPath);
|
|
66
|
+
mergeInfo.mergeCommitMessage = commitResponse.message || null;
|
|
67
|
+
}
|
|
68
|
+
catch (commitError) {
|
|
69
|
+
console.error('Failed to fetch merge commit message:', commitError);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (activitiesError) {
|
|
75
|
+
console.error('Failed to fetch PR activities:', activitiesError);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let comments = [];
|
|
79
|
+
let activeCommentCount = 0;
|
|
80
|
+
let totalCommentCount = 0;
|
|
81
|
+
let fileChanges = [];
|
|
82
|
+
let fileChangesSummary = null;
|
|
83
|
+
try {
|
|
84
|
+
const [commentsResult, fileChangesResult] = await Promise.all([
|
|
85
|
+
this.fetchPullRequestComments(workspace, repository, pull_request_id),
|
|
86
|
+
this.fetchPullRequestFileChanges(workspace, repository, pull_request_id)
|
|
87
|
+
]);
|
|
88
|
+
comments = commentsResult.comments;
|
|
89
|
+
activeCommentCount = commentsResult.activeCount;
|
|
90
|
+
totalCommentCount = commentsResult.totalCount;
|
|
91
|
+
fileChanges = fileChangesResult.fileChanges;
|
|
92
|
+
fileChangesSummary = fileChangesResult.summary;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('Failed to fetch additional PR data:', error);
|
|
96
|
+
}
|
|
97
|
+
const formattedResponse = this.apiClient.getIsServer()
|
|
98
|
+
? formatServerResponse(pr, mergeInfo, this.baseUrl)
|
|
99
|
+
: formatCloudResponse(pr);
|
|
100
|
+
const enhancedResponse = {
|
|
101
|
+
...formattedResponse,
|
|
102
|
+
active_comments: comments,
|
|
103
|
+
active_comment_count: activeCommentCount,
|
|
104
|
+
total_comment_count: totalCommentCount,
|
|
105
|
+
file_changes: fileChanges,
|
|
106
|
+
file_changes_summary: fileChangesSummary
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify(enhancedResponse, null, 2),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return this.apiClient.handleApiError(error, `getting pull request ${pull_request_id} in ${workspace}/${repository}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async handleListPullRequests(args) {
|
|
122
|
+
if (!isListPullRequestsArgs(args)) {
|
|
123
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_pull_requests');
|
|
124
|
+
}
|
|
125
|
+
const { workspace, repository, state = 'OPEN', author, limit = 25, start = 0 } = args;
|
|
126
|
+
try {
|
|
127
|
+
let apiPath;
|
|
128
|
+
let params = {};
|
|
129
|
+
if (this.apiClient.getIsServer()) {
|
|
130
|
+
// Bitbucket Server API
|
|
131
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
|
|
132
|
+
params = {
|
|
133
|
+
state: state === 'ALL' ? undefined : state,
|
|
134
|
+
limit,
|
|
135
|
+
start,
|
|
136
|
+
};
|
|
137
|
+
if (author) {
|
|
138
|
+
params['role.1'] = 'AUTHOR';
|
|
139
|
+
params['username.1'] = author;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Bitbucket Cloud API
|
|
144
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
|
|
145
|
+
params = {
|
|
146
|
+
state: state === 'ALL' ? undefined : state,
|
|
147
|
+
pagelen: limit,
|
|
148
|
+
page: Math.floor(start / limit) + 1,
|
|
149
|
+
};
|
|
150
|
+
if (author) {
|
|
151
|
+
params['q'] = `author.username="${author}"`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
|
|
155
|
+
let pullRequests = [];
|
|
156
|
+
let totalCount = 0;
|
|
157
|
+
let nextPageStart = null;
|
|
158
|
+
if (this.apiClient.getIsServer()) {
|
|
159
|
+
pullRequests = (response.values || []).map((pr) => formatServerResponse(pr, undefined, this.baseUrl));
|
|
160
|
+
totalCount = response.size || 0;
|
|
161
|
+
if (!response.isLastPage && response.nextPageStart !== undefined) {
|
|
162
|
+
nextPageStart = response.nextPageStart;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
pullRequests = (response.values || []).map((pr) => formatCloudResponse(pr));
|
|
167
|
+
totalCount = response.size || 0;
|
|
168
|
+
if (response.next) {
|
|
169
|
+
nextPageStart = start + limit;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: JSON.stringify({
|
|
177
|
+
pull_requests: pullRequests,
|
|
178
|
+
total_count: totalCount,
|
|
179
|
+
start,
|
|
180
|
+
limit,
|
|
181
|
+
has_more: nextPageStart !== null,
|
|
182
|
+
next_start: nextPageStart,
|
|
183
|
+
}, null, 2),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return this.apiClient.handleApiError(error, `listing pull requests in ${workspace}/${repository}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async handleCreatePullRequest(args) {
|
|
193
|
+
if (!isCreatePullRequestArgs(args)) {
|
|
194
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for create_pull_request');
|
|
195
|
+
}
|
|
196
|
+
const { workspace, repository, title, source_branch, destination_branch, description, reviewers, close_source_branch } = args;
|
|
197
|
+
try {
|
|
198
|
+
let apiPath;
|
|
199
|
+
let requestBody;
|
|
200
|
+
if (this.apiClient.getIsServer()) {
|
|
201
|
+
// Bitbucket Server API
|
|
202
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
|
|
203
|
+
requestBody = {
|
|
204
|
+
title,
|
|
205
|
+
description: description || '',
|
|
206
|
+
fromRef: {
|
|
207
|
+
id: `refs/heads/${source_branch}`,
|
|
208
|
+
repository: {
|
|
209
|
+
slug: repository,
|
|
210
|
+
project: {
|
|
211
|
+
key: workspace
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
toRef: {
|
|
216
|
+
id: `refs/heads/${destination_branch}`,
|
|
217
|
+
repository: {
|
|
218
|
+
slug: repository,
|
|
219
|
+
project: {
|
|
220
|
+
key: workspace
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
reviewers: reviewers?.map(r => ({ user: { name: r } })) || []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Bitbucket Cloud API
|
|
229
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
|
|
230
|
+
requestBody = {
|
|
231
|
+
title,
|
|
232
|
+
description: description || '',
|
|
233
|
+
source: {
|
|
234
|
+
branch: {
|
|
235
|
+
name: source_branch
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
destination: {
|
|
239
|
+
branch: {
|
|
240
|
+
name: destination_branch
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
close_source_branch: close_source_branch || false,
|
|
244
|
+
reviewers: reviewers?.map(r => ({ username: r })) || []
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const pr = await this.apiClient.makeRequest('post', apiPath, requestBody);
|
|
248
|
+
const formattedResponse = this.apiClient.getIsServer()
|
|
249
|
+
? formatServerResponse(pr, undefined, this.baseUrl)
|
|
250
|
+
: formatCloudResponse(pr);
|
|
251
|
+
return {
|
|
252
|
+
content: [
|
|
253
|
+
{
|
|
254
|
+
type: 'text',
|
|
255
|
+
text: JSON.stringify({
|
|
256
|
+
message: 'Pull request created successfully',
|
|
257
|
+
pull_request: formattedResponse
|
|
258
|
+
}, null, 2),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return this.apiClient.handleApiError(error, `creating pull request in ${workspace}/${repository}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async handleUpdatePullRequest(args) {
|
|
268
|
+
if (!isUpdatePullRequestArgs(args)) {
|
|
269
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for update_pull_request');
|
|
270
|
+
}
|
|
271
|
+
const { workspace, repository, pull_request_id, title, description, destination_branch, reviewers } = args;
|
|
272
|
+
try {
|
|
273
|
+
let apiPath;
|
|
274
|
+
let requestBody = {};
|
|
275
|
+
if (this.apiClient.getIsServer()) {
|
|
276
|
+
// Bitbucket Server API
|
|
277
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
|
|
278
|
+
// First get the current PR to get version number and existing data
|
|
279
|
+
const currentPr = await this.apiClient.makeRequest('get', apiPath);
|
|
280
|
+
requestBody.version = currentPr.version;
|
|
281
|
+
if (title !== undefined)
|
|
282
|
+
requestBody.title = title;
|
|
283
|
+
if (description !== undefined)
|
|
284
|
+
requestBody.description = description;
|
|
285
|
+
if (destination_branch !== undefined) {
|
|
286
|
+
requestBody.toRef = {
|
|
287
|
+
id: `refs/heads/${destination_branch}`,
|
|
288
|
+
repository: {
|
|
289
|
+
slug: repository,
|
|
290
|
+
project: {
|
|
291
|
+
key: workspace
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Handle reviewers: preserve existing ones if not explicitly updating
|
|
297
|
+
if (reviewers !== undefined) {
|
|
298
|
+
// User wants to update reviewers
|
|
299
|
+
// Create a map of existing reviewers for preservation of approval status
|
|
300
|
+
const existingReviewersMap = new Map(currentPr.reviewers.map((r) => [r.user.name, r]));
|
|
301
|
+
requestBody.reviewers = reviewers.map(username => {
|
|
302
|
+
const existing = existingReviewersMap.get(username);
|
|
303
|
+
if (existing) {
|
|
304
|
+
// Preserve existing reviewer's full data including approval status
|
|
305
|
+
return existing;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Add new reviewer (without approval status)
|
|
309
|
+
return { user: { name: username } };
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// No reviewers provided - preserve existing reviewers with their full data
|
|
315
|
+
requestBody.reviewers = currentPr.reviewers;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Bitbucket Cloud API
|
|
320
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
|
|
321
|
+
if (title !== undefined)
|
|
322
|
+
requestBody.title = title;
|
|
323
|
+
if (description !== undefined)
|
|
324
|
+
requestBody.description = description;
|
|
325
|
+
if (destination_branch !== undefined) {
|
|
326
|
+
requestBody.destination = {
|
|
327
|
+
branch: {
|
|
328
|
+
name: destination_branch
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (reviewers !== undefined) {
|
|
333
|
+
requestBody.reviewers = reviewers.map(r => ({ username: r }));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const pr = await this.apiClient.makeRequest('put', apiPath, requestBody);
|
|
337
|
+
const formattedResponse = this.apiClient.getIsServer()
|
|
338
|
+
? formatServerResponse(pr, undefined, this.baseUrl)
|
|
339
|
+
: formatCloudResponse(pr);
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: 'text',
|
|
344
|
+
text: JSON.stringify({
|
|
345
|
+
message: 'Pull request updated successfully',
|
|
346
|
+
pull_request: formattedResponse
|
|
347
|
+
}, null, 2),
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return this.apiClient.handleApiError(error, `updating pull request ${pull_request_id} in ${workspace}/${repository}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async handleAddComment(args) {
|
|
357
|
+
if (!isAddCommentArgs(args)) {
|
|
358
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for add_comment');
|
|
359
|
+
}
|
|
360
|
+
let { workspace, repository, pull_request_id, comment_text, parent_comment_id, file_path, line_number, line_type, suggestion, suggestion_end_line, code_snippet, search_context, match_strategy = 'strict' } = args;
|
|
361
|
+
let sequentialPosition;
|
|
362
|
+
if (code_snippet && !line_number && file_path) {
|
|
363
|
+
try {
|
|
364
|
+
const resolved = await this.resolveLineFromCode(workspace, repository, pull_request_id, file_path, code_snippet, search_context, match_strategy);
|
|
365
|
+
line_number = resolved.line_number;
|
|
366
|
+
line_type = resolved.line_type;
|
|
367
|
+
sequentialPosition = resolved.sequential_position;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (suggestion && (!file_path || !line_number)) {
|
|
374
|
+
throw new McpError(ErrorCode.InvalidParams, 'Suggestions require file_path and line_number to be specified');
|
|
375
|
+
}
|
|
376
|
+
const isInlineComment = file_path !== undefined && line_number !== undefined;
|
|
377
|
+
let finalCommentText = comment_text;
|
|
378
|
+
if (suggestion) {
|
|
379
|
+
finalCommentText = formatSuggestionComment(comment_text, suggestion, line_number, suggestion_end_line || line_number);
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
let apiPath;
|
|
383
|
+
let requestBody;
|
|
384
|
+
if (this.apiClient.getIsServer()) {
|
|
385
|
+
// Bitbucket Server API
|
|
386
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
|
|
387
|
+
requestBody = {
|
|
388
|
+
text: finalCommentText
|
|
389
|
+
};
|
|
390
|
+
if (parent_comment_id !== undefined) {
|
|
391
|
+
requestBody.parent = { id: parent_comment_id };
|
|
392
|
+
}
|
|
393
|
+
if (isInlineComment) {
|
|
394
|
+
requestBody.anchor = {
|
|
395
|
+
line: line_number,
|
|
396
|
+
lineType: line_type || 'CONTEXT',
|
|
397
|
+
fileType: line_type === 'REMOVED' ? 'FROM' : 'TO',
|
|
398
|
+
path: file_path,
|
|
399
|
+
diffType: 'EFFECTIVE'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// Bitbucket Cloud API
|
|
405
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
|
|
406
|
+
requestBody = {
|
|
407
|
+
content: {
|
|
408
|
+
raw: finalCommentText
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
if (parent_comment_id !== undefined) {
|
|
412
|
+
requestBody.parent = { id: parent_comment_id };
|
|
413
|
+
}
|
|
414
|
+
if (isInlineComment) {
|
|
415
|
+
requestBody.inline = {
|
|
416
|
+
to: line_number,
|
|
417
|
+
path: file_path
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const comment = await this.apiClient.makeRequest('post', apiPath, requestBody);
|
|
422
|
+
const responseMessage = suggestion
|
|
423
|
+
? 'Comment with code suggestion added successfully'
|
|
424
|
+
: (isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully');
|
|
425
|
+
return {
|
|
426
|
+
content: [
|
|
427
|
+
{
|
|
428
|
+
type: 'text',
|
|
429
|
+
text: JSON.stringify({
|
|
430
|
+
message: responseMessage,
|
|
431
|
+
comment: {
|
|
432
|
+
id: comment.id,
|
|
433
|
+
text: this.apiClient.getIsServer() ? comment.text : comment.content.raw,
|
|
434
|
+
author: this.apiClient.getIsServer() ? comment.author.displayName : comment.user.display_name,
|
|
435
|
+
created_on: this.apiClient.getIsServer() ? new Date(comment.createdDate).toLocaleString() : comment.created_on,
|
|
436
|
+
file_path: isInlineComment ? file_path : undefined,
|
|
437
|
+
line_number: isInlineComment ? line_number : undefined,
|
|
438
|
+
line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined,
|
|
439
|
+
has_suggestion: !!suggestion,
|
|
440
|
+
suggestion_lines: suggestion ? (suggestion_end_line ? `${line_number}-${suggestion_end_line}` : `${line_number}`) : undefined
|
|
441
|
+
}
|
|
442
|
+
}, null, 2),
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
return this.apiClient.handleApiError(error, `adding ${isInlineComment ? 'inline ' : ''}comment to pull request ${pull_request_id} in ${workspace}/${repository}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async handleMergePullRequest(args) {
|
|
452
|
+
if (!isMergePullRequestArgs(args)) {
|
|
453
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for merge_pull_request');
|
|
454
|
+
}
|
|
455
|
+
const { workspace, repository, pull_request_id, merge_strategy, close_source_branch, commit_message } = args;
|
|
456
|
+
try {
|
|
457
|
+
let apiPath;
|
|
458
|
+
let requestBody = {};
|
|
459
|
+
if (this.apiClient.getIsServer()) {
|
|
460
|
+
// Bitbucket Server API
|
|
461
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/merge`;
|
|
462
|
+
// Get current PR version
|
|
463
|
+
const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
|
|
464
|
+
const currentPr = await this.apiClient.makeRequest('get', prPath);
|
|
465
|
+
requestBody.version = currentPr.version;
|
|
466
|
+
if (commit_message) {
|
|
467
|
+
requestBody.message = commit_message;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
// Bitbucket Cloud API
|
|
472
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/merge`;
|
|
473
|
+
if (merge_strategy) {
|
|
474
|
+
requestBody.merge_strategy = merge_strategy;
|
|
475
|
+
}
|
|
476
|
+
if (close_source_branch !== undefined) {
|
|
477
|
+
requestBody.close_source_branch = close_source_branch;
|
|
478
|
+
}
|
|
479
|
+
if (commit_message) {
|
|
480
|
+
requestBody.message = commit_message;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const result = await this.apiClient.makeRequest('post', apiPath, requestBody);
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
type: 'text',
|
|
488
|
+
text: JSON.stringify({
|
|
489
|
+
message: 'Pull request merged successfully',
|
|
490
|
+
merge_commit: this.apiClient.getIsServer() ? result.properties?.mergeCommit : result.merge_commit?.hash,
|
|
491
|
+
pull_request_id
|
|
492
|
+
}, null, 2),
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return this.apiClient.handleApiError(error, `merging pull request ${pull_request_id} in ${workspace}/${repository}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async fetchPullRequestComments(workspace, repository, pullRequestId) {
|
|
502
|
+
try {
|
|
503
|
+
let comments = [];
|
|
504
|
+
let activeCount = 0;
|
|
505
|
+
let totalCount = 0;
|
|
506
|
+
if (this.apiClient.getIsServer()) {
|
|
507
|
+
const processNestedComments = (comment, anchor) => {
|
|
508
|
+
const formattedComment = {
|
|
509
|
+
id: comment.id,
|
|
510
|
+
author: comment.author.displayName,
|
|
511
|
+
text: comment.text,
|
|
512
|
+
created_on: new Date(comment.createdDate).toISOString(),
|
|
513
|
+
is_inline: !!anchor,
|
|
514
|
+
file_path: anchor?.path,
|
|
515
|
+
line_number: anchor?.line,
|
|
516
|
+
state: comment.state
|
|
517
|
+
};
|
|
518
|
+
if (comment.comments && comment.comments.length > 0) {
|
|
519
|
+
formattedComment.replies = comment.comments
|
|
520
|
+
.filter((reply) => {
|
|
521
|
+
if (reply.state === 'RESOLVED')
|
|
522
|
+
return false;
|
|
523
|
+
if (anchor && anchor.orphaned === true)
|
|
524
|
+
return false;
|
|
525
|
+
return true;
|
|
526
|
+
})
|
|
527
|
+
.map((reply) => processNestedComments(reply, anchor));
|
|
528
|
+
}
|
|
529
|
+
return formattedComment;
|
|
530
|
+
};
|
|
531
|
+
const countAllComments = (comment) => {
|
|
532
|
+
let count = 1;
|
|
533
|
+
if (comment.comments && comment.comments.length > 0) {
|
|
534
|
+
count += comment.comments.reduce((sum, reply) => sum + countAllComments(reply), 0);
|
|
535
|
+
}
|
|
536
|
+
return count;
|
|
537
|
+
};
|
|
538
|
+
const countActiveComments = (comment, anchor) => {
|
|
539
|
+
let count = 0;
|
|
540
|
+
if (comment.state !== 'RESOLVED' && (!anchor || anchor.orphaned !== true)) {
|
|
541
|
+
count = 1;
|
|
542
|
+
}
|
|
543
|
+
if (comment.comments && comment.comments.length > 0) {
|
|
544
|
+
count += comment.comments.reduce((sum, reply) => sum + countActiveComments(reply, anchor), 0);
|
|
545
|
+
}
|
|
546
|
+
return count;
|
|
547
|
+
};
|
|
548
|
+
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/activities`;
|
|
549
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
|
|
550
|
+
params: { limit: 1000 }
|
|
551
|
+
});
|
|
552
|
+
const activities = response.values || [];
|
|
553
|
+
const commentActivities = activities.filter((a) => a.action === 'COMMENTED' && a.comment);
|
|
554
|
+
totalCount = commentActivities.reduce((sum, activity) => {
|
|
555
|
+
return sum + countAllComments(activity.comment);
|
|
556
|
+
}, 0);
|
|
557
|
+
activeCount = commentActivities.reduce((sum, activity) => {
|
|
558
|
+
return sum + countActiveComments(activity.comment, activity.commentAnchor);
|
|
559
|
+
}, 0);
|
|
560
|
+
const processedComments = commentActivities
|
|
561
|
+
.filter((a) => {
|
|
562
|
+
const c = a.comment;
|
|
563
|
+
const anchor = a.commentAnchor;
|
|
564
|
+
if (c.state === 'RESOLVED')
|
|
565
|
+
return false;
|
|
566
|
+
if (anchor && anchor.orphaned === true)
|
|
567
|
+
return false;
|
|
568
|
+
return true;
|
|
569
|
+
})
|
|
570
|
+
.map((a) => processNestedComments(a.comment, a.commentAnchor));
|
|
571
|
+
comments = processedComments.slice(0, 20);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/comments`;
|
|
575
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
|
|
576
|
+
params: { pagelen: 100 }
|
|
577
|
+
});
|
|
578
|
+
const allComments = response.values || [];
|
|
579
|
+
totalCount = allComments.length;
|
|
580
|
+
const activeComments = allComments
|
|
581
|
+
.filter((c) => !c.deleted && !c.resolved)
|
|
582
|
+
.slice(0, 20);
|
|
583
|
+
activeCount = allComments.filter((c) => !c.deleted && !c.resolved).length;
|
|
584
|
+
comments = activeComments.map((c) => ({
|
|
585
|
+
id: c.id,
|
|
586
|
+
author: c.user.display_name,
|
|
587
|
+
text: c.content.raw,
|
|
588
|
+
created_on: c.created_on,
|
|
589
|
+
is_inline: !!c.inline,
|
|
590
|
+
file_path: c.inline?.path,
|
|
591
|
+
line_number: c.inline?.to
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
return { comments, activeCount, totalCount };
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
console.error('Failed to fetch comments:', error);
|
|
598
|
+
return { comments: [], activeCount: 0, totalCount: 0 };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async fetchPullRequestFileChanges(workspace, repository, pullRequestId) {
|
|
602
|
+
try {
|
|
603
|
+
let fileChanges = [];
|
|
604
|
+
let totalLinesAdded = 0;
|
|
605
|
+
let totalLinesRemoved = 0;
|
|
606
|
+
if (this.apiClient.getIsServer()) {
|
|
607
|
+
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/changes`;
|
|
608
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
|
|
609
|
+
params: { limit: 1000 }
|
|
610
|
+
});
|
|
611
|
+
const changes = response.values || [];
|
|
612
|
+
fileChanges = changes.map((change) => {
|
|
613
|
+
let status = 'modified';
|
|
614
|
+
if (change.type === 'ADD')
|
|
615
|
+
status = 'added';
|
|
616
|
+
else if (change.type === 'DELETE')
|
|
617
|
+
status = 'removed';
|
|
618
|
+
else if (change.type === 'MOVE' || change.type === 'RENAME')
|
|
619
|
+
status = 'renamed';
|
|
620
|
+
return {
|
|
621
|
+
path: change.path.toString,
|
|
622
|
+
status,
|
|
623
|
+
old_path: change.srcPath?.toString
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diffstat`;
|
|
629
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, {
|
|
630
|
+
params: { pagelen: 100 }
|
|
631
|
+
});
|
|
632
|
+
const diffstats = response.values || [];
|
|
633
|
+
fileChanges = diffstats.map((stat) => {
|
|
634
|
+
totalLinesAdded += stat.lines_added;
|
|
635
|
+
totalLinesRemoved += stat.lines_removed;
|
|
636
|
+
return {
|
|
637
|
+
path: stat.path,
|
|
638
|
+
status: stat.type,
|
|
639
|
+
old_path: stat.old?.path
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const summary = {
|
|
644
|
+
total_files: fileChanges.length
|
|
645
|
+
};
|
|
646
|
+
return { fileChanges, summary };
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
console.error('Failed to fetch file changes:', error);
|
|
650
|
+
return {
|
|
651
|
+
fileChanges: [],
|
|
652
|
+
summary: {
|
|
653
|
+
total_files: 0
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async resolveLineFromCode(workspace, repository, pullRequestId, filePath, codeSnippet, searchContext, matchStrategy = 'strict') {
|
|
659
|
+
try {
|
|
660
|
+
const diffContent = await this.getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath);
|
|
661
|
+
const parser = new DiffParser();
|
|
662
|
+
const sections = parser.parseDiffIntoSections(diffContent);
|
|
663
|
+
let fileSection = sections[0];
|
|
664
|
+
if (!this.apiClient.getIsServer()) {
|
|
665
|
+
fileSection = sections.find(s => s.filePath === filePath) || sections[0];
|
|
666
|
+
}
|
|
667
|
+
if (!fileSection) {
|
|
668
|
+
throw new McpError(ErrorCode.InvalidParams, `File ${filePath} not found in pull request diff`);
|
|
669
|
+
}
|
|
670
|
+
const matches = this.findCodeMatches(fileSection.content, codeSnippet, searchContext);
|
|
671
|
+
if (matches.length === 0) {
|
|
672
|
+
throw new McpError(ErrorCode.InvalidParams, `Code snippet not found in ${filePath}`);
|
|
673
|
+
}
|
|
674
|
+
if (matches.length === 1) {
|
|
675
|
+
return {
|
|
676
|
+
line_number: matches[0].line_number,
|
|
677
|
+
line_type: matches[0].line_type,
|
|
678
|
+
sequential_position: matches[0].sequential_position,
|
|
679
|
+
hunk_info: matches[0].hunk_info,
|
|
680
|
+
diff_context: matches[0].preview,
|
|
681
|
+
diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
|
|
682
|
+
calculation_details: `Direct line number from diff: ${matches[0].line_number}`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (matchStrategy === 'best') {
|
|
686
|
+
const best = this.selectBestMatch(matches);
|
|
687
|
+
return {
|
|
688
|
+
line_number: best.line_number,
|
|
689
|
+
line_type: best.line_type,
|
|
690
|
+
sequential_position: best.sequential_position,
|
|
691
|
+
hunk_info: best.hunk_info,
|
|
692
|
+
diff_context: best.preview,
|
|
693
|
+
diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
|
|
694
|
+
calculation_details: `Best match selected from ${matches.length} matches, line: ${best.line_number}`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const error = {
|
|
698
|
+
code: 'MULTIPLE_MATCHES_FOUND',
|
|
699
|
+
message: `Code snippet '${codeSnippet.substring(0, 50)}...' found in ${matches.length} locations`,
|
|
700
|
+
occurrences: matches.map(m => ({
|
|
701
|
+
line_number: m.line_number,
|
|
702
|
+
file_path: filePath,
|
|
703
|
+
preview: m.preview,
|
|
704
|
+
confidence: m.confidence,
|
|
705
|
+
line_type: m.line_type
|
|
706
|
+
})),
|
|
707
|
+
suggestion: 'To resolve, either:\n1. Add more context to uniquely identify the location\n2. Use match_strategy: \'best\' to auto-select highest confidence match\n3. Use line_number directly'
|
|
708
|
+
};
|
|
709
|
+
throw new McpError(ErrorCode.InvalidParams, JSON.stringify({ error }));
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (error instanceof McpError) {
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
throw new McpError(ErrorCode.InternalError, `Failed to resolve line from code: ${error instanceof Error ? error.message : String(error)}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
findCodeMatches(diffContent, codeSnippet, searchContext) {
|
|
719
|
+
const lines = diffContent.split('\n');
|
|
720
|
+
const matches = [];
|
|
721
|
+
let currentDestLine = 0; // Destination file line number
|
|
722
|
+
let currentSrcLine = 0; // Source file line number
|
|
723
|
+
let inHunk = false;
|
|
724
|
+
let sequentialAddedCount = 0; // Track sequential ADDED lines
|
|
725
|
+
let currentHunkIndex = -1;
|
|
726
|
+
let currentHunkDestStart = 0;
|
|
727
|
+
let currentHunkSrcStart = 0;
|
|
728
|
+
let destPositionInHunk = 0; // Track position in destination file relative to hunk start
|
|
729
|
+
let srcPositionInHunk = 0; // Track position in source file relative to hunk start
|
|
730
|
+
for (let i = 0; i < lines.length; i++) {
|
|
731
|
+
const line = lines[i];
|
|
732
|
+
if (line.startsWith('@@')) {
|
|
733
|
+
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
|
|
734
|
+
if (match) {
|
|
735
|
+
currentHunkSrcStart = parseInt(match[1]);
|
|
736
|
+
currentHunkDestStart = parseInt(match[2]);
|
|
737
|
+
currentSrcLine = currentHunkSrcStart;
|
|
738
|
+
currentDestLine = currentHunkDestStart;
|
|
739
|
+
inHunk = true;
|
|
740
|
+
currentHunkIndex++;
|
|
741
|
+
destPositionInHunk = 0;
|
|
742
|
+
srcPositionInHunk = 0;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!inHunk)
|
|
747
|
+
continue;
|
|
748
|
+
if (line === '') {
|
|
749
|
+
inHunk = false;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
let lineType;
|
|
753
|
+
let lineContent = '';
|
|
754
|
+
let lineNumber = 0;
|
|
755
|
+
if (line.startsWith('+')) {
|
|
756
|
+
lineType = 'ADDED';
|
|
757
|
+
lineContent = line.substring(1);
|
|
758
|
+
lineNumber = currentHunkDestStart + destPositionInHunk;
|
|
759
|
+
destPositionInHunk++;
|
|
760
|
+
sequentialAddedCount++;
|
|
761
|
+
}
|
|
762
|
+
else if (line.startsWith('-')) {
|
|
763
|
+
lineType = 'REMOVED';
|
|
764
|
+
lineContent = line.substring(1);
|
|
765
|
+
lineNumber = currentHunkSrcStart + srcPositionInHunk;
|
|
766
|
+
srcPositionInHunk++;
|
|
767
|
+
}
|
|
768
|
+
else if (line.startsWith(' ')) {
|
|
769
|
+
lineType = 'CONTEXT';
|
|
770
|
+
lineContent = line.substring(1);
|
|
771
|
+
lineNumber = currentHunkDestStart + destPositionInHunk;
|
|
772
|
+
destPositionInHunk++;
|
|
773
|
+
srcPositionInHunk++;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
inHunk = false;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (lineContent.trim() === codeSnippet.trim()) {
|
|
780
|
+
const confidence = this.calculateConfidence(lines, i, searchContext, lineType);
|
|
781
|
+
matches.push({
|
|
782
|
+
line_number: lineNumber,
|
|
783
|
+
line_type: lineType,
|
|
784
|
+
exact_content: codeSnippet,
|
|
785
|
+
preview: this.getPreview(lines, i),
|
|
786
|
+
confidence,
|
|
787
|
+
context: this.extractContext(lines, i),
|
|
788
|
+
sequential_position: lineType === 'ADDED' ? sequentialAddedCount : undefined,
|
|
789
|
+
hunk_info: {
|
|
790
|
+
hunk_index: currentHunkIndex,
|
|
791
|
+
destination_start: currentHunkDestStart,
|
|
792
|
+
line_in_hunk: destPositionInHunk
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
if (lineType === 'ADDED') {
|
|
797
|
+
currentDestLine++;
|
|
798
|
+
}
|
|
799
|
+
else if (lineType === 'REMOVED') {
|
|
800
|
+
currentSrcLine++;
|
|
801
|
+
}
|
|
802
|
+
else if (lineType === 'CONTEXT') {
|
|
803
|
+
currentSrcLine++;
|
|
804
|
+
currentDestLine++;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return matches;
|
|
808
|
+
}
|
|
809
|
+
calculateConfidence(lines, index, searchContext, lineType) {
|
|
810
|
+
let confidence = 0.5; // Base confidence
|
|
811
|
+
if (!searchContext) {
|
|
812
|
+
return confidence;
|
|
813
|
+
}
|
|
814
|
+
if (searchContext.before) {
|
|
815
|
+
let matchedBefore = 0;
|
|
816
|
+
for (let j = 0; j < searchContext.before.length; j++) {
|
|
817
|
+
const contextLine = searchContext.before[searchContext.before.length - 1 - j];
|
|
818
|
+
const checkIndex = index - j - 1;
|
|
819
|
+
if (checkIndex >= 0) {
|
|
820
|
+
const checkLine = lines[checkIndex].substring(1);
|
|
821
|
+
if (checkLine.trim() === contextLine.trim()) {
|
|
822
|
+
matchedBefore++;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
confidence += (matchedBefore / searchContext.before.length) * 0.3;
|
|
827
|
+
}
|
|
828
|
+
if (searchContext.after) {
|
|
829
|
+
let matchedAfter = 0;
|
|
830
|
+
for (let j = 0; j < searchContext.after.length; j++) {
|
|
831
|
+
const contextLine = searchContext.after[j];
|
|
832
|
+
const checkIndex = index + j + 1;
|
|
833
|
+
if (checkIndex < lines.length) {
|
|
834
|
+
const checkLine = lines[checkIndex].substring(1);
|
|
835
|
+
if (checkLine.trim() === contextLine.trim()) {
|
|
836
|
+
matchedAfter++;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
confidence += (matchedAfter / searchContext.after.length) * 0.3;
|
|
841
|
+
}
|
|
842
|
+
if (lineType === 'ADDED') {
|
|
843
|
+
confidence += 0.1;
|
|
844
|
+
}
|
|
845
|
+
return Math.min(confidence, 1.0);
|
|
846
|
+
}
|
|
847
|
+
getPreview(lines, index) {
|
|
848
|
+
const start = Math.max(0, index - 1);
|
|
849
|
+
const end = Math.min(lines.length, index + 2);
|
|
850
|
+
const previewLines = [];
|
|
851
|
+
for (let i = start; i < end; i++) {
|
|
852
|
+
const prefix = i === index ? '> ' : ' ';
|
|
853
|
+
previewLines.push(prefix + lines[i]);
|
|
854
|
+
}
|
|
855
|
+
return previewLines.join('\n');
|
|
856
|
+
}
|
|
857
|
+
extractContext(lines, index) {
|
|
858
|
+
const linesBefore = [];
|
|
859
|
+
const linesAfter = [];
|
|
860
|
+
for (let i = Math.max(0, index - 2); i < index; i++) {
|
|
861
|
+
if (lines[i].match(/^[+\- ]/)) {
|
|
862
|
+
linesBefore.push(lines[i].substring(1));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (let i = index + 1; i < Math.min(lines.length, index + 3); i++) {
|
|
866
|
+
if (lines[i].match(/^[+\- ]/)) {
|
|
867
|
+
linesAfter.push(lines[i].substring(1));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
lines_before: linesBefore,
|
|
872
|
+
lines_after: linesAfter
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
selectBestMatch(matches) {
|
|
876
|
+
return matches.sort((a, b) => b.confidence - a.confidence)[0];
|
|
877
|
+
}
|
|
878
|
+
async handleListPrCommits(args) {
|
|
879
|
+
if (!isListPrCommitsArgs(args)) {
|
|
880
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for list_pr_commits');
|
|
881
|
+
}
|
|
882
|
+
const { workspace, repository, pull_request_id, limit = 25, start = 0 } = args;
|
|
883
|
+
try {
|
|
884
|
+
// First get the PR details to include in response
|
|
885
|
+
const prPath = this.apiClient.getIsServer()
|
|
886
|
+
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
|
|
887
|
+
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
|
|
888
|
+
let prTitle = '';
|
|
889
|
+
try {
|
|
890
|
+
const pr = await this.apiClient.makeRequest('get', prPath);
|
|
891
|
+
prTitle = pr.title;
|
|
892
|
+
}
|
|
893
|
+
catch (e) {
|
|
894
|
+
// Ignore error, PR title is optional
|
|
895
|
+
}
|
|
896
|
+
let apiPath;
|
|
897
|
+
let params = {};
|
|
898
|
+
let commits = [];
|
|
899
|
+
let totalCount = 0;
|
|
900
|
+
let nextPageStart = null;
|
|
901
|
+
if (this.apiClient.getIsServer()) {
|
|
902
|
+
// Bitbucket Server API
|
|
903
|
+
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/commits`;
|
|
904
|
+
params = {
|
|
905
|
+
limit,
|
|
906
|
+
start,
|
|
907
|
+
withCounts: true
|
|
908
|
+
};
|
|
909
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
|
|
910
|
+
// Format commits
|
|
911
|
+
commits = (response.values || []).map((commit) => formatServerCommit(commit));
|
|
912
|
+
totalCount = response.size || commits.length;
|
|
913
|
+
if (!response.isLastPage && response.nextPageStart !== undefined) {
|
|
914
|
+
nextPageStart = response.nextPageStart;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// Bitbucket Cloud API
|
|
919
|
+
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/commits`;
|
|
920
|
+
params = {
|
|
921
|
+
pagelen: limit,
|
|
922
|
+
page: Math.floor(start / limit) + 1
|
|
923
|
+
};
|
|
924
|
+
const response = await this.apiClient.makeRequest('get', apiPath, undefined, { params });
|
|
925
|
+
// Format commits
|
|
926
|
+
commits = (response.values || []).map((commit) => formatCloudCommit(commit));
|
|
927
|
+
totalCount = response.size || commits.length;
|
|
928
|
+
if (response.next) {
|
|
929
|
+
nextPageStart = start + limit;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
content: [
|
|
934
|
+
{
|
|
935
|
+
type: 'text',
|
|
936
|
+
text: JSON.stringify({
|
|
937
|
+
pull_request_id,
|
|
938
|
+
pull_request_title: prTitle,
|
|
939
|
+
commits,
|
|
940
|
+
total_count: totalCount,
|
|
941
|
+
start,
|
|
942
|
+
limit,
|
|
943
|
+
has_more: nextPageStart !== null,
|
|
944
|
+
next_start: nextPageStart
|
|
945
|
+
}, null, 2),
|
|
946
|
+
},
|
|
947
|
+
],
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
return this.apiClient.handleApiError(error, `listing commits for pull request ${pull_request_id} in ${workspace}/${repository}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
//# sourceMappingURL=pull-request-handlers.js.map
|