@atlassian-dc-mcp/bitbucket 0.32.0 → 0.33.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/CHANGELOG.md +11 -0
- package/README.md +19 -0
- package/build/__tests__/bitbucket-service.test.js +102 -0
- package/build/__tests__/bitbucket-service.test.js.map +1 -1
- package/build/__tests__/compare-diff-mapper.test.d.ts +2 -0
- package/build/__tests__/compare-diff-mapper.test.d.ts.map +1 -0
- package/build/__tests__/compare-diff-mapper.test.js +104 -0
- package/build/__tests__/compare-diff-mapper.test.js.map +1 -0
- package/build/bitbucket-service.d.ts +31 -0
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +58 -0
- package/build/bitbucket-service.js.map +1 -1
- package/build/compare-diff-mapper.d.ts +42 -0
- package/build/compare-diff-mapper.d.ts.map +1 -0
- package/build/compare-diff-mapper.js +60 -0
- package/build/compare-diff-mapper.js.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/bitbucket-service.test.ts +162 -0
- package/src/__tests__/compare-diff-mapper.test.ts +116 -0
- package/src/bitbucket-service.ts +74 -0
- package/src/compare-diff-mapper.ts +99 -0
- package/src/index.ts +10 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1212,6 +1212,168 @@ describe('BitbucketService', () => {
|
|
|
1212
1212
|
});
|
|
1213
1213
|
});
|
|
1214
1214
|
|
|
1215
|
+
describe('getBranchDiff', () => {
|
|
1216
|
+
const { request: mockRequest } = require('../bitbucket-client/core/request.js');
|
|
1217
|
+
// Trimmed capture of a real Bitbucket DC 9.x compare/diff response.
|
|
1218
|
+
const mockCompareDiff = {
|
|
1219
|
+
fromHash: '4ab70c4ea742bd2102e929a3b58811a69a46ff23',
|
|
1220
|
+
toHash: 'feature/my-branch',
|
|
1221
|
+
diffs: [
|
|
1222
|
+
{
|
|
1223
|
+
source: { toString: 'Dockerfile' },
|
|
1224
|
+
destination: { toString: 'Dockerfile' },
|
|
1225
|
+
hunks: [
|
|
1226
|
+
{
|
|
1227
|
+
sourceLine: 1,
|
|
1228
|
+
sourceSpan: 2,
|
|
1229
|
+
destinationLine: 1,
|
|
1230
|
+
destinationSpan: 3,
|
|
1231
|
+
segments: [
|
|
1232
|
+
{ type: 'CONTEXT', lines: [{ line: 'FROM node:20-alpine' }] },
|
|
1233
|
+
{ type: 'ADDED', lines: [{ line: 'COPY shbdn ./shbdn' }] },
|
|
1234
|
+
{ type: 'REMOVED', lines: [{ line: 'RUN npm ci' }] }
|
|
1235
|
+
]
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
}
|
|
1239
|
+
],
|
|
1240
|
+
truncated: false
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
it('should diff the whole comparison against the default branch when only a source branch is given', async () => {
|
|
1244
|
+
mockRequest.mockResolvedValue(mockCompareDiff);
|
|
1245
|
+
|
|
1246
|
+
const result = await bitbucketService.getBranchDiff(
|
|
1247
|
+
mockProjectKey,
|
|
1248
|
+
mockRepositorySlug,
|
|
1249
|
+
'feature/my-branch'
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
expect(result.success).toBe(true);
|
|
1253
|
+
expect(result.data).toBe(
|
|
1254
|
+
'diff --git a/Dockerfile b/Dockerfile\n' +
|
|
1255
|
+
'--- a/Dockerfile\n' +
|
|
1256
|
+
'+++ b/Dockerfile\n' +
|
|
1257
|
+
'@@ -1,2 +1,3 @@\n' +
|
|
1258
|
+
' FROM node:20-alpine\n' +
|
|
1259
|
+
'+COPY shbdn ./shbdn\n' +
|
|
1260
|
+
'-RUN npm ci'
|
|
1261
|
+
);
|
|
1262
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
1263
|
+
expect.any(Object),
|
|
1264
|
+
{
|
|
1265
|
+
method: 'GET',
|
|
1266
|
+
url: '/api/latest/projects/{projectKey}/repos/{repositorySlug}/compare/diff{path}',
|
|
1267
|
+
path: {
|
|
1268
|
+
'path': '',
|
|
1269
|
+
'projectKey': mockProjectKey,
|
|
1270
|
+
'repositorySlug': mockRepositorySlug,
|
|
1271
|
+
},
|
|
1272
|
+
query: {
|
|
1273
|
+
'from': 'feature/my-branch',
|
|
1274
|
+
'to': undefined,
|
|
1275
|
+
'contextLines': undefined,
|
|
1276
|
+
'srcPath': undefined,
|
|
1277
|
+
'whitespace': undefined,
|
|
1278
|
+
},
|
|
1279
|
+
errors: {
|
|
1280
|
+
401: `The currently authenticated user has insufficient permissions to view the repository.`,
|
|
1281
|
+
404: `The repository, or one of the compared refs, does not exist.`,
|
|
1282
|
+
},
|
|
1283
|
+
}
|
|
1284
|
+
);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it('should not request text/plain, which the compare resource rejects with 406', async () => {
|
|
1288
|
+
mockRequest.mockResolvedValue(mockCompareDiff);
|
|
1289
|
+
|
|
1290
|
+
await bitbucketService.getBranchDiff(mockProjectKey, mockRepositorySlug, 'feature/my-branch');
|
|
1291
|
+
|
|
1292
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
1293
|
+
expect.any(Object),
|
|
1294
|
+
expect.not.objectContaining({ headers: expect.anything() })
|
|
1295
|
+
);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
it('should pass all optional parameters through and normalize the file path', async () => {
|
|
1299
|
+
mockRequest.mockResolvedValue(mockCompareDiff);
|
|
1300
|
+
|
|
1301
|
+
const result = await bitbucketService.getBranchDiff(
|
|
1302
|
+
mockProjectKey,
|
|
1303
|
+
mockRepositorySlug,
|
|
1304
|
+
'feature/my-branch',
|
|
1305
|
+
'main',
|
|
1306
|
+
'/src/file.txt',
|
|
1307
|
+
'5',
|
|
1308
|
+
'old/file.txt',
|
|
1309
|
+
'ignore-all'
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1312
|
+
expect(result.success).toBe(true);
|
|
1313
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
1314
|
+
expect.any(Object),
|
|
1315
|
+
expect.objectContaining({
|
|
1316
|
+
path: expect.objectContaining({ 'path': '/src/file.txt' }),
|
|
1317
|
+
query: {
|
|
1318
|
+
'from': 'feature/my-branch',
|
|
1319
|
+
'to': 'main',
|
|
1320
|
+
'contextLines': '5',
|
|
1321
|
+
'srcPath': 'old/file.txt',
|
|
1322
|
+
'whitespace': 'ignore-all',
|
|
1323
|
+
},
|
|
1324
|
+
})
|
|
1325
|
+
);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it('should return the raw payload when output is full', async () => {
|
|
1329
|
+
mockRequest.mockResolvedValue(mockCompareDiff);
|
|
1330
|
+
|
|
1331
|
+
const result = await bitbucketService.getBranchDiff(
|
|
1332
|
+
mockProjectKey,
|
|
1333
|
+
mockRepositorySlug,
|
|
1334
|
+
'feature/my-branch',
|
|
1335
|
+
undefined,
|
|
1336
|
+
undefined,
|
|
1337
|
+
undefined,
|
|
1338
|
+
undefined,
|
|
1339
|
+
undefined,
|
|
1340
|
+
'full'
|
|
1341
|
+
);
|
|
1342
|
+
|
|
1343
|
+
expect(result.success).toBe(true);
|
|
1344
|
+
expect(result.data).toEqual(mockCompareDiff);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it('should normalize project key and repository slug casing', async () => {
|
|
1348
|
+
mockRequest.mockResolvedValue(mockCompareDiff);
|
|
1349
|
+
|
|
1350
|
+
await bitbucketService.getBranchDiff('test', 'TEST-REPO', 'feature/my-branch');
|
|
1351
|
+
|
|
1352
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
1353
|
+
expect.any(Object),
|
|
1354
|
+
expect.objectContaining({
|
|
1355
|
+
path: expect.objectContaining({
|
|
1356
|
+
'projectKey': 'TEST',
|
|
1357
|
+
'repositorySlug': 'test-repo',
|
|
1358
|
+
}),
|
|
1359
|
+
})
|
|
1360
|
+
);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it('should handle API errors gracefully', async () => {
|
|
1364
|
+
mockRequest.mockRejectedValue(new Error('API Error'));
|
|
1365
|
+
|
|
1366
|
+
const result = await bitbucketService.getBranchDiff(
|
|
1367
|
+
mockProjectKey,
|
|
1368
|
+
mockRepositorySlug,
|
|
1369
|
+
'feature/my-branch'
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
expect(result.success).toBe(false);
|
|
1373
|
+
expect(result.error).toBe('API Error');
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1215
1377
|
describe('getFileContent', () => {
|
|
1216
1378
|
const mockFileContent = 'FROM node:20-alpine\n\nWORKDIR /app\n';
|
|
1217
1379
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { formatCompareDiffAsUnified } from '../compare-diff-mapper.js';
|
|
2
|
+
|
|
3
|
+
describe('formatCompareDiffAsUnified', () => {
|
|
4
|
+
it('should report an empty comparison', () => {
|
|
5
|
+
expect(formatCompareDiffAsUnified({ diffs: [] })).toBe('No differences found.');
|
|
6
|
+
expect(formatCompareDiffAsUnified({})).toBe('No differences found.');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should render added and removed files against /dev/null', () => {
|
|
10
|
+
const result = formatCompareDiffAsUnified({
|
|
11
|
+
diffs: [
|
|
12
|
+
{
|
|
13
|
+
source: null,
|
|
14
|
+
destination: { toString: 'new-file.js' },
|
|
15
|
+
hunks: [
|
|
16
|
+
{
|
|
17
|
+
sourceLine: 0,
|
|
18
|
+
sourceSpan: 0,
|
|
19
|
+
destinationLine: 1,
|
|
20
|
+
destinationSpan: 1,
|
|
21
|
+
segments: [{ type: 'ADDED', lines: [{ line: 'export const a = 1;' }] }]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
source: { toString: 'old-file.js' },
|
|
27
|
+
destination: null,
|
|
28
|
+
hunks: [
|
|
29
|
+
{
|
|
30
|
+
sourceLine: 1,
|
|
31
|
+
sourceSpan: 1,
|
|
32
|
+
destinationLine: 0,
|
|
33
|
+
destinationSpan: 0,
|
|
34
|
+
segments: [{ type: 'REMOVED', lines: [{ line: 'export const b = 2;' }] }]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result).toBe(
|
|
42
|
+
'diff --git a/new-file.js b/new-file.js\n' +
|
|
43
|
+
'--- /dev/null\n' +
|
|
44
|
+
'+++ b/new-file.js\n' +
|
|
45
|
+
'@@ -0,0 +1,1 @@\n' +
|
|
46
|
+
'+export const a = 1;\n' +
|
|
47
|
+
'diff --git a/old-file.js b/old-file.js\n' +
|
|
48
|
+
'--- a/old-file.js\n' +
|
|
49
|
+
'+++ /dev/null\n' +
|
|
50
|
+
'@@ -1,1 +0,0 @@\n' +
|
|
51
|
+
'-export const b = 2;'
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should note files without a textual diff', () => {
|
|
56
|
+
const result = formatCompareDiffAsUnified({
|
|
57
|
+
diffs: [{ source: { toString: 'logo.png' }, destination: { toString: 'logo.png' } }]
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result).toContain('[no textual diff available]');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should surface server-side truncation at hunk, file and diff level', () => {
|
|
64
|
+
const result = formatCompareDiffAsUnified({
|
|
65
|
+
truncated: true,
|
|
66
|
+
diffs: [
|
|
67
|
+
{
|
|
68
|
+
source: { toString: 'big.txt' },
|
|
69
|
+
destination: { toString: 'big.txt' },
|
|
70
|
+
truncated: true,
|
|
71
|
+
hunks: [{ sourceLine: 1, sourceSpan: 1, destinationLine: 1, destinationSpan: 1, segments: [], truncated: true }]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(result).toContain('[hunk truncated by the Bitbucket server]');
|
|
77
|
+
expect(result).toContain('[file diff truncated by the Bitbucket server]');
|
|
78
|
+
expect(result).toContain('[diff truncated by the Bitbucket server; narrow the comparison with `path`]');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should prefix segments by type and default unknown types to context', () => {
|
|
82
|
+
const result = formatCompareDiffAsUnified({
|
|
83
|
+
diffs: [
|
|
84
|
+
{
|
|
85
|
+
source: { toString: 'f.txt' },
|
|
86
|
+
destination: { toString: 'f.txt' },
|
|
87
|
+
hunks: [
|
|
88
|
+
{
|
|
89
|
+
sourceLine: 10,
|
|
90
|
+
sourceSpan: 3,
|
|
91
|
+
destinationLine: 10,
|
|
92
|
+
destinationSpan: 4,
|
|
93
|
+
segments: [
|
|
94
|
+
{ type: 'CONTEXT', lines: [{ line: 'keep' }] },
|
|
95
|
+
{ type: 'REMOVED', lines: [{ line: 'gone' }] },
|
|
96
|
+
{ type: 'ADDED', lines: [{ line: 'fresh' }] },
|
|
97
|
+
{ type: 'SOMETHING_NEW', lines: [{ line: 'unknown' }] }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result).toBe(
|
|
106
|
+
'diff --git a/f.txt b/f.txt\n' +
|
|
107
|
+
'--- a/f.txt\n' +
|
|
108
|
+
'+++ b/f.txt\n' +
|
|
109
|
+
'@@ -10,3 +10,4 @@\n' +
|
|
110
|
+
' keep\n' +
|
|
111
|
+
'-gone\n' +
|
|
112
|
+
'+fresh\n' +
|
|
113
|
+
' unknown'
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/bitbucket-service.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { OpenAPI, ProjectService, PullRequestsService, RepositoryService } from
|
|
|
3
3
|
import { request as __request } from './bitbucket-client/core/request.js';
|
|
4
4
|
import { handleApiOperation, resolveOpenApiBase } from '@atlassian-dc-mcp/common';
|
|
5
5
|
import { simplifyInboxPullRequests } from './inbox-pr-mapper.js';
|
|
6
|
+
import { CompareDiffResponse, formatCompareDiffAsUnified } from './compare-diff-mapper.js';
|
|
6
7
|
import { BITBUCKET_PRODUCT, getDefaultPageSize, getMissingConfig } from './config.js';
|
|
7
8
|
import { fetchMergeability, mergePullRequest, type MergePullRequestParams } from './pr-merge.js';
|
|
8
9
|
import {
|
|
@@ -26,6 +27,8 @@ function resolveToken(token: string | (() => string | undefined), missingTokenMe
|
|
|
26
27
|
|
|
27
28
|
type DiffLineType = 'ADDED' | 'REMOVED' | 'CONTEXT';
|
|
28
29
|
|
|
30
|
+
type BranchDiffOutputMode = 'unified' | 'full';
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* Build a Bitbucket DC inline comment anchor.
|
|
31
34
|
*
|
|
@@ -704,6 +707,66 @@ export class BitbucketService {
|
|
|
704
707
|
);
|
|
705
708
|
}
|
|
706
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Get text diff between two branches, with or without an existing pull request.
|
|
712
|
+
*
|
|
713
|
+
* Mirrors the Bitbucket "compare" view: the diff contains changes reachable from
|
|
714
|
+
* `sourceBranch` but not from `targetBranch`.
|
|
715
|
+
* @param projectKey The project key
|
|
716
|
+
* @param repositorySlug The repository slug
|
|
717
|
+
* @param sourceBranch The source branch, tag or commit
|
|
718
|
+
* @param targetBranch Optional target branch, tag or commit. Defaults to the repository default branch
|
|
719
|
+
* @param path Optional file path to limit the diff to a single file
|
|
720
|
+
* @param contextLines Optional number of context lines to include around added/removed lines
|
|
721
|
+
* @param srcPath Optional previous path to the file, if the file has been copied, moved or renamed
|
|
722
|
+
* @param whitespace Optional whitespace flag which can be set to 'ignore-all'
|
|
723
|
+
* @param output Render a unified diff or return the raw RestDiff payload. Defaults to 'unified'
|
|
724
|
+
* @returns Promise with the diff between the two refs
|
|
725
|
+
*/
|
|
726
|
+
async getBranchDiff(
|
|
727
|
+
projectKey: string,
|
|
728
|
+
repositorySlug: string,
|
|
729
|
+
sourceBranch: string,
|
|
730
|
+
targetBranch?: string,
|
|
731
|
+
path?: string,
|
|
732
|
+
contextLines?: string,
|
|
733
|
+
srcPath?: string,
|
|
734
|
+
whitespace?: string,
|
|
735
|
+
output: BranchDiffOutputMode = 'unified'
|
|
736
|
+
) {
|
|
737
|
+
projectKey = projectKey.toUpperCase();
|
|
738
|
+
repositorySlug = repositorySlug.toLowerCase();
|
|
739
|
+
// The endpoint is `/compare/diff{path}`: an empty path diffs the whole comparison,
|
|
740
|
+
// a file path must keep its leading slash to stay a separate URL segment.
|
|
741
|
+
const trimmedPath = path?.replace(/^\/+/, '') ?? '';
|
|
742
|
+
return handleApiOperation(
|
|
743
|
+
async () => {
|
|
744
|
+
const diff = await __request<CompareDiffResponse>(OpenAPI, {
|
|
745
|
+
method: 'GET',
|
|
746
|
+
url: '/api/latest/projects/{projectKey}/repos/{repositorySlug}/compare/diff{path}',
|
|
747
|
+
path: {
|
|
748
|
+
'path': trimmedPath ? `/${trimmedPath}` : '',
|
|
749
|
+
'projectKey': projectKey,
|
|
750
|
+
'repositorySlug': repositorySlug,
|
|
751
|
+
},
|
|
752
|
+
query: {
|
|
753
|
+
'from': sourceBranch,
|
|
754
|
+
'to': targetBranch,
|
|
755
|
+
'contextLines': contextLines,
|
|
756
|
+
'srcPath': srcPath,
|
|
757
|
+
'whitespace': whitespace,
|
|
758
|
+
},
|
|
759
|
+
errors: {
|
|
760
|
+
401: `The currently authenticated user has insufficient permissions to view the repository.`,
|
|
761
|
+
404: `The repository, or one of the compared refs, does not exist.`,
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
return output === 'full' ? diff : formatCompareDiffAsUnified(diff);
|
|
765
|
+
},
|
|
766
|
+
'Error fetching branch diff'
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
707
770
|
/**
|
|
708
771
|
* Create a pull request
|
|
709
772
|
* @param projectKey The project key (also used as the destination repository's project)
|
|
@@ -1133,6 +1196,17 @@ export const bitbucketToolSchemas = {
|
|
|
1133
1196
|
untilId: z.string().optional().describe("The until commit hash to stream a diff between two arbitrary hashes"),
|
|
1134
1197
|
whitespace: z.string().optional().describe("Optional whitespace flag which can be set to 'ignore-all'")
|
|
1135
1198
|
},
|
|
1199
|
+
getBranchDiff: {
|
|
1200
|
+
projectKey: z.string().describe("The project key"),
|
|
1201
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1202
|
+
sourceBranch: z.string().describe("The source branch, tag or commit to diff (e.g. 'feature/my-branch'). Changes reachable from here but not from targetBranch are returned"),
|
|
1203
|
+
targetBranch: z.string().optional().describe("The target branch, tag or commit to compare against. Defaults to the repository's default branch"),
|
|
1204
|
+
path: z.string().optional().describe("Limit the diff to a single file path. Omit to get the diff for every changed file"),
|
|
1205
|
+
contextLines: z.string().optional().describe("Number of context lines to include around added/removed lines in the diff"),
|
|
1206
|
+
srcPath: z.string().optional().describe("The previous path to the file, if the file has been copied, moved or renamed"),
|
|
1207
|
+
whitespace: z.string().optional().describe("Optional whitespace flag which can be set to 'ignore-all'"),
|
|
1208
|
+
output: z.enum(['unified', 'full']).optional().describe("Render the comparison as a unified diff or return the raw RestDiff payload. Defaults to unified.")
|
|
1209
|
+
},
|
|
1136
1210
|
createPullRequest: {
|
|
1137
1211
|
projectKey: z.string().describe("The destination repository's project key"),
|
|
1138
1212
|
repositorySlug: z.string().describe("The destination repository's slug"),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Shape of the `RestDiff` payload returned by the Bitbucket compare/diff resource.
|
|
2
|
+
interface DiffLine {
|
|
3
|
+
line: string;
|
|
4
|
+
truncated?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface DiffSegment {
|
|
8
|
+
type: string;
|
|
9
|
+
lines?: DiffLine[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DiffHunk {
|
|
13
|
+
sourceLine?: number;
|
|
14
|
+
sourceSpan?: number;
|
|
15
|
+
destinationLine?: number;
|
|
16
|
+
destinationSpan?: number;
|
|
17
|
+
segments?: DiffSegment[];
|
|
18
|
+
truncated?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FileDiff {
|
|
22
|
+
source?: { toString: string } | null;
|
|
23
|
+
destination?: { toString: string } | null;
|
|
24
|
+
hunks?: DiffHunk[];
|
|
25
|
+
truncated?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CompareDiffResponse {
|
|
29
|
+
fromHash?: string;
|
|
30
|
+
toHash?: string;
|
|
31
|
+
diffs?: FileDiff[];
|
|
32
|
+
truncated?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SEGMENT_PREFIXES: Record<string, string> = {
|
|
36
|
+
ADDED: '+',
|
|
37
|
+
REMOVED: '-',
|
|
38
|
+
CONTEXT: ' ',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function renderHunk(hunk: DiffHunk, out: string[]): void {
|
|
42
|
+
out.push(`@@ -${hunk.sourceLine ?? 0},${hunk.sourceSpan ?? 0} +${hunk.destinationLine ?? 0},${hunk.destinationSpan ?? 0} @@`);
|
|
43
|
+
for (const segment of hunk.segments ?? []) {
|
|
44
|
+
const prefix = SEGMENT_PREFIXES[segment.type] ?? ' ';
|
|
45
|
+
for (const line of segment.lines ?? []) {
|
|
46
|
+
out.push(`${prefix}${line.line}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (hunk.truncated) {
|
|
50
|
+
out.push('[hunk truncated by the Bitbucket server]');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderFile(file: FileDiff, out: string[]): void {
|
|
55
|
+
// A missing source means the file was added, a missing destination that it was deleted.
|
|
56
|
+
const source = file.source?.toString;
|
|
57
|
+
const destination = file.destination?.toString;
|
|
58
|
+
const oldPath = source ? `a/${source}` : '/dev/null';
|
|
59
|
+
const newPath = destination ? `b/${destination}` : '/dev/null';
|
|
60
|
+
|
|
61
|
+
out.push(`diff --git ${source ? `a/${source}` : `a/${destination}`} ${destination ? `b/${destination}` : `b/${source}`}`);
|
|
62
|
+
out.push(`--- ${oldPath}`);
|
|
63
|
+
out.push(`+++ ${newPath}`);
|
|
64
|
+
|
|
65
|
+
const hunks = file.hunks ?? [];
|
|
66
|
+
if (hunks.length === 0) {
|
|
67
|
+
// Bitbucket omits hunks for binary files and for pure mode/rename changes.
|
|
68
|
+
out.push('[no textual diff available]');
|
|
69
|
+
}
|
|
70
|
+
for (const hunk of hunks) {
|
|
71
|
+
renderHunk(hunk, out);
|
|
72
|
+
}
|
|
73
|
+
if (file.truncated) {
|
|
74
|
+
out.push('[file diff truncated by the Bitbucket server]');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a Bitbucket `RestDiff` payload as a unified diff.
|
|
80
|
+
*
|
|
81
|
+
* The compare resource only speaks JSON (it answers 406 to `Accept: text/plain`, unlike the
|
|
82
|
+
* pull request diff resource), so the structured payload is folded into the far more compact
|
|
83
|
+
* unified format that models already understand.
|
|
84
|
+
*/
|
|
85
|
+
export function formatCompareDiffAsUnified(diff: CompareDiffResponse): string {
|
|
86
|
+
const files = diff.diffs ?? [];
|
|
87
|
+
if (files.length === 0) {
|
|
88
|
+
return 'No differences found.';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const out: string[] = [];
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
renderFile(file, out);
|
|
94
|
+
}
|
|
95
|
+
if (diff.truncated) {
|
|
96
|
+
out.push('[diff truncated by the Bitbucket server; narrow the comparison with `path`]');
|
|
97
|
+
}
|
|
98
|
+
return out.join('\n');
|
|
99
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -179,6 +179,16 @@ server.tool(
|
|
|
179
179
|
}
|
|
180
180
|
);
|
|
181
181
|
|
|
182
|
+
server.tool(
|
|
183
|
+
"bitbucket_getBranchDiff",
|
|
184
|
+
"Get the diff between two branches, tags or commits — including branches that have no pull request yet. Returns the same comparison as the Bitbucket 'compare' view: changes reachable from sourceBranch but not from targetBranch, rendered as a unified diff. targetBranch defaults to the repository's default branch. Use this to review work in progress before a PR exists; once a PR exists, prefer bitbucket_getPullRequestChanges + bitbucket_getPullRequestDiff.",
|
|
185
|
+
bitbucketToolSchemas.getBranchDiff,
|
|
186
|
+
async ({ projectKey, repositorySlug, sourceBranch, targetBranch, path, contextLines, srcPath, whitespace, output }) => {
|
|
187
|
+
const result = await bitbucketService.getBranchDiff(projectKey, repositorySlug, sourceBranch, targetBranch, path, contextLines, srcPath, whitespace, output);
|
|
188
|
+
return formatToolResponse(result);
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
182
192
|
server.tool(
|
|
183
193
|
"bitbucket_createPullRequest",
|
|
184
194
|
"Create a new pull request in a Bitbucket repository. Supports fork-based pull requests by passing fromProjectKey/fromRepositorySlug when the source repository differs from the destination (projectKey/repositorySlug). IMPORTANT: Before creating a PR, use bitbucket_getRequiredReviewers to fetch required reviewers for the source and target branches to ensure the PR is not created without mandatory reviewers.",
|