@atlassian-dc-mcp/bitbucket 0.24.0 → 0.25.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 +19 -0
- package/build/__tests__/bitbucket-service.test.js +166 -16
- package/build/__tests__/bitbucket-service.test.js.map +1 -1
- package/build/__tests__/bitbucket-token-optimization.test.js +1 -1
- package/build/__tests__/bitbucket-token-optimization.test.js.map +1 -1
- package/build/bitbucket-response-mapper.js +3 -3
- package/build/bitbucket-response-mapper.js.map +1 -1
- package/build/bitbucket-service.d.ts +38 -3
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +130 -24
- package/build/bitbucket-service.js.map +1 -1
- package/build/index.js +6 -2
- package/build/index.js.map +1 -1
- package/build/pr-comment-mapper.d.ts +2 -0
- package/build/pr-comment-mapper.d.ts.map +1 -1
- package/build/pr-comment-mapper.js +7 -1
- package/build/pr-comment-mapper.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/bitbucket-service.test.ts +304 -16
- package/src/__tests__/bitbucket-token-optimization.test.ts +1 -1
- package/src/bitbucket-response-mapper.ts +3 -3
- package/src/bitbucket-service.ts +175 -25
- package/src/index.ts +13 -3
- package/src/pr-comment-mapper.ts +13 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -16,6 +16,7 @@ jest.mock('../bitbucket-client/index.js', () => ({
|
|
|
16
16
|
PullRequestsService: {
|
|
17
17
|
streamRawDiff2: jest.fn(),
|
|
18
18
|
createComment2: jest.fn(),
|
|
19
|
+
updateComment2: jest.fn(),
|
|
19
20
|
streamChanges1: jest.fn(),
|
|
20
21
|
create: jest.fn(),
|
|
21
22
|
update: jest.fn(),
|
|
@@ -860,10 +861,8 @@ describe('BitbucketService', () => {
|
|
|
860
861
|
line: 15,
|
|
861
862
|
lineType: 'ADDED',
|
|
862
863
|
fileType: 'TO',
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
multilineStartLineType: 'ADDED',
|
|
866
|
-
multilineDestinationRange: { minimum: 10, maximum: 15 }
|
|
864
|
+
multilineMarker: { startLine: 10, startLineType: 'ADDED' },
|
|
865
|
+
multilineSpan: { dstSpanStart: 10, dstSpanEnd: 15 }
|
|
867
866
|
}
|
|
868
867
|
};
|
|
869
868
|
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
@@ -905,10 +904,8 @@ describe('BitbucketService', () => {
|
|
|
905
904
|
line: 15,
|
|
906
905
|
lineType: 'ADDED',
|
|
907
906
|
fileType: 'TO',
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
multilineStartLineType: 'ADDED',
|
|
911
|
-
multilineDestinationRange: { minimum: 10, maximum: 15 }
|
|
907
|
+
multilineMarker: { startLine: 10, startLineType: 'ADDED' },
|
|
908
|
+
multilineSpan: { dstSpanStart: 10, dstSpanEnd: 15 }
|
|
912
909
|
}
|
|
913
910
|
}
|
|
914
911
|
);
|
|
@@ -925,10 +922,8 @@ describe('BitbucketService', () => {
|
|
|
925
922
|
line: 8,
|
|
926
923
|
lineType: 'ADDED',
|
|
927
924
|
fileType: 'TO',
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
multilineStartLineType: 'CONTEXT',
|
|
931
|
-
multilineDestinationRange: { minimum: 5, maximum: 8 }
|
|
925
|
+
multilineMarker: { startLine: 5, startLineType: 'CONTEXT' },
|
|
926
|
+
multilineSpan: { dstSpanStart: 5, dstSpanEnd: 8 }
|
|
932
927
|
}
|
|
933
928
|
};
|
|
934
929
|
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
@@ -958,15 +953,133 @@ describe('BitbucketService', () => {
|
|
|
958
953
|
line: 8,
|
|
959
954
|
lineType: 'ADDED',
|
|
960
955
|
fileType: 'TO',
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
multilineStartLineType: 'CONTEXT',
|
|
964
|
-
multilineDestinationRange: { minimum: 5, maximum: 8 }
|
|
956
|
+
multilineMarker: { startLine: 5, startLineType: 'CONTEXT' },
|
|
957
|
+
multilineSpan: { dstSpanStart: 5, dstSpanEnd: 8 }
|
|
965
958
|
}
|
|
966
959
|
}
|
|
967
960
|
);
|
|
968
961
|
});
|
|
969
962
|
|
|
963
|
+
it('should normalize a reversed multiline range (startLine > line)', async () => {
|
|
964
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue({ id: 1, anchor: { path: 'src/test.js' } });
|
|
965
|
+
|
|
966
|
+
await bitbucketService.postPullRequestComment(
|
|
967
|
+
mockProjectKey,
|
|
968
|
+
mockRepositorySlug,
|
|
969
|
+
mockPullRequestId,
|
|
970
|
+
'Reversed range',
|
|
971
|
+
undefined, // parentId
|
|
972
|
+
'src/test.js', // filePath
|
|
973
|
+
15, // startLine (greater than line)
|
|
974
|
+
'ADDED', // startLineType
|
|
975
|
+
10, // line (end line, smaller)
|
|
976
|
+
'ADDED' // lineType
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
|
|
980
|
+
mockProjectKey,
|
|
981
|
+
mockPullRequestId,
|
|
982
|
+
mockRepositorySlug,
|
|
983
|
+
{
|
|
984
|
+
text: 'Reversed range',
|
|
985
|
+
anchor: {
|
|
986
|
+
path: 'src/test.js',
|
|
987
|
+
diffType: 'EFFECTIVE',
|
|
988
|
+
line: 15,
|
|
989
|
+
lineType: 'ADDED',
|
|
990
|
+
fileType: 'TO',
|
|
991
|
+
multilineMarker: { startLine: 10, startLineType: 'ADDED' },
|
|
992
|
+
multilineSpan: { dstSpanStart: 10, dstSpanEnd: 15 }
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('should anchor a REMOVED multiline range to the source file', async () => {
|
|
999
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue({ id: 1, anchor: { path: 'src/test.js' } });
|
|
1000
|
+
|
|
1001
|
+
await bitbucketService.postPullRequestComment(
|
|
1002
|
+
mockProjectKey,
|
|
1003
|
+
mockRepositorySlug,
|
|
1004
|
+
mockPullRequestId,
|
|
1005
|
+
'Removed range',
|
|
1006
|
+
undefined, // parentId
|
|
1007
|
+
'src/test.js', // filePath
|
|
1008
|
+
2, // startLine
|
|
1009
|
+
'REMOVED', // startLineType
|
|
1010
|
+
5, // line (end line)
|
|
1011
|
+
'REMOVED' // lineType
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
|
|
1015
|
+
mockProjectKey,
|
|
1016
|
+
mockPullRequestId,
|
|
1017
|
+
mockRepositorySlug,
|
|
1018
|
+
{
|
|
1019
|
+
text: 'Removed range',
|
|
1020
|
+
anchor: {
|
|
1021
|
+
path: 'src/test.js',
|
|
1022
|
+
diffType: 'EFFECTIVE',
|
|
1023
|
+
line: 5,
|
|
1024
|
+
lineType: 'REMOVED',
|
|
1025
|
+
fileType: 'FROM',
|
|
1026
|
+
multilineMarker: { startLine: 2, startLineType: 'REMOVED' },
|
|
1027
|
+
multilineSpan: { srcSpanStart: 2, srcSpanEnd: 5 }
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
it('should warn when a multi-line suggestion is anchored to a single line', async () => {
|
|
1034
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue({
|
|
1035
|
+
id: 99,
|
|
1036
|
+
anchor: { path: 'src/test.js', line: 5, lineType: 'ADDED' }
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
const result = await bitbucketService.postPullRequestComment(
|
|
1040
|
+
mockProjectKey,
|
|
1041
|
+
mockRepositorySlug,
|
|
1042
|
+
mockPullRequestId,
|
|
1043
|
+
'```suggestion\nconst a = 1;\nconst b = 2;\n```',
|
|
1044
|
+
undefined, // parentId
|
|
1045
|
+
'src/test.js', // filePath
|
|
1046
|
+
undefined, // startLine (single-line anchor — the bug shape)
|
|
1047
|
+
undefined, // startLineType
|
|
1048
|
+
5, // line
|
|
1049
|
+
'ADDED' // lineType
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
expect(result.success).toBe(true);
|
|
1053
|
+
expect((result.data as any).warning).toMatch(/multi-line ```suggestion/);
|
|
1054
|
+
// single-line anchor: no multiline fields sent
|
|
1055
|
+
const sentAnchor = (PullRequestsService.createComment2 as jest.Mock).mock.calls[0][3].anchor;
|
|
1056
|
+
expect(sentAnchor.multilineMarker).toBeUndefined();
|
|
1057
|
+
expect(sentAnchor.multilineSpan).toBeUndefined();
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('should not warn when a multi-line suggestion has a proper multiline range', async () => {
|
|
1061
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue({
|
|
1062
|
+
id: 100,
|
|
1063
|
+
anchor: { path: 'src/test.js', line: 5, lineType: 'ADDED', multilineMarker: { startLine: 4, startLineType: 'ADDED' } }
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
const result = await bitbucketService.postPullRequestComment(
|
|
1067
|
+
mockProjectKey,
|
|
1068
|
+
mockRepositorySlug,
|
|
1069
|
+
mockPullRequestId,
|
|
1070
|
+
'```suggestion\nconst a = 1;\nconst b = 2;\n```',
|
|
1071
|
+
undefined, // parentId
|
|
1072
|
+
'src/test.js', // filePath
|
|
1073
|
+
4, // startLine
|
|
1074
|
+
'ADDED', // startLineType
|
|
1075
|
+
5, // line
|
|
1076
|
+
'ADDED' // lineType
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
expect(result.success).toBe(true);
|
|
1080
|
+
expect((result.data as any).warning).toBeUndefined();
|
|
1081
|
+
});
|
|
1082
|
+
|
|
970
1083
|
it('should handle API errors gracefully', async () => {
|
|
971
1084
|
const mockError = new Error('API Error');
|
|
972
1085
|
(PullRequestsService.createComment2 as jest.Mock).mockRejectedValue(mockError);
|
|
@@ -2002,6 +2115,181 @@ describe('BitbucketService', () => {
|
|
|
2002
2115
|
});
|
|
2003
2116
|
});
|
|
2004
2117
|
|
|
2118
|
+
describe('updatePullRequestComment', () => {
|
|
2119
|
+
it('should resolve a task by sending state RESOLVED with the version', async () => {
|
|
2120
|
+
const mockComment = { id: 500, version: 2, state: 'RESOLVED', text: 'Task body' };
|
|
2121
|
+
(PullRequestsService.updateComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
2122
|
+
|
|
2123
|
+
const result = await bitbucketService.updatePullRequestComment(
|
|
2124
|
+
mockProjectKey,
|
|
2125
|
+
mockRepositorySlug,
|
|
2126
|
+
mockPullRequestId,
|
|
2127
|
+
'500', // commentId
|
|
2128
|
+
1, // version
|
|
2129
|
+
undefined, // text
|
|
2130
|
+
'RESOLVED' // state
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
expect(result.success).toBe(true);
|
|
2134
|
+
expect(PullRequestsService.updateComment2).toHaveBeenCalledWith(
|
|
2135
|
+
mockProjectKey,
|
|
2136
|
+
'500',
|
|
2137
|
+
mockPullRequestId,
|
|
2138
|
+
mockRepositorySlug,
|
|
2139
|
+
{ version: 1, state: 'RESOLVED' }
|
|
2140
|
+
);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
it('should edit the comment text without changing state or severity', async () => {
|
|
2144
|
+
const mockComment = { id: 501, version: 3, text: 'Edited text' };
|
|
2145
|
+
(PullRequestsService.updateComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
2146
|
+
|
|
2147
|
+
await bitbucketService.updatePullRequestComment(
|
|
2148
|
+
mockProjectKey,
|
|
2149
|
+
mockRepositorySlug,
|
|
2150
|
+
mockPullRequestId,
|
|
2151
|
+
'501',
|
|
2152
|
+
2,
|
|
2153
|
+
'Edited text'
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
expect(PullRequestsService.updateComment2).toHaveBeenCalledWith(
|
|
2157
|
+
mockProjectKey,
|
|
2158
|
+
'501',
|
|
2159
|
+
mockPullRequestId,
|
|
2160
|
+
mockRepositorySlug,
|
|
2161
|
+
{ version: 2, text: 'Edited text' }
|
|
2162
|
+
);
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
it('should support combining text, state, and severity in a single update', async () => {
|
|
2166
|
+
const mockComment = { id: 502, version: 4, text: 'New body', state: 'RESOLVED', severity: 'BLOCKER' };
|
|
2167
|
+
(PullRequestsService.updateComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
2168
|
+
|
|
2169
|
+
await bitbucketService.updatePullRequestComment(
|
|
2170
|
+
mockProjectKey,
|
|
2171
|
+
mockRepositorySlug,
|
|
2172
|
+
mockPullRequestId,
|
|
2173
|
+
'502',
|
|
2174
|
+
3,
|
|
2175
|
+
'New body',
|
|
2176
|
+
'RESOLVED',
|
|
2177
|
+
'BLOCKER'
|
|
2178
|
+
);
|
|
2179
|
+
|
|
2180
|
+
expect(PullRequestsService.updateComment2).toHaveBeenCalledWith(
|
|
2181
|
+
mockProjectKey,
|
|
2182
|
+
'502',
|
|
2183
|
+
mockPullRequestId,
|
|
2184
|
+
mockRepositorySlug,
|
|
2185
|
+
{ version: 3, text: 'New body', state: 'RESOLVED', severity: 'BLOCKER' }
|
|
2186
|
+
);
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
it('should propagate API errors', async () => {
|
|
2190
|
+
(PullRequestsService.updateComment2 as jest.Mock).mockRejectedValue(new Error('Conflict'));
|
|
2191
|
+
|
|
2192
|
+
const result = await bitbucketService.updatePullRequestComment(
|
|
2193
|
+
mockProjectKey,
|
|
2194
|
+
mockRepositorySlug,
|
|
2195
|
+
mockPullRequestId,
|
|
2196
|
+
'503',
|
|
2197
|
+
1,
|
|
2198
|
+
undefined,
|
|
2199
|
+
'RESOLVED'
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
expect(result.success).toBe(false);
|
|
2203
|
+
expect(result.error).toBe('Conflict');
|
|
2204
|
+
});
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
describe('postPullRequestComment - severity flag', () => {
|
|
2208
|
+
it('should include severity: BLOCKER in the request body when severity is BLOCKER', async () => {
|
|
2209
|
+
const mockComment = { id: 200, text: 'Task comment', author: { displayName: 'Test User' } };
|
|
2210
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
2211
|
+
|
|
2212
|
+
const result = await bitbucketService.postPullRequestComment(
|
|
2213
|
+
mockProjectKey,
|
|
2214
|
+
mockRepositorySlug,
|
|
2215
|
+
mockPullRequestId,
|
|
2216
|
+
'Task comment',
|
|
2217
|
+
undefined, // parentId
|
|
2218
|
+
undefined, // filePath
|
|
2219
|
+
undefined, // startLine
|
|
2220
|
+
undefined, // startLineType
|
|
2221
|
+
undefined, // line
|
|
2222
|
+
undefined, // lineType
|
|
2223
|
+
undefined, // pending
|
|
2224
|
+
'BLOCKER' // severity
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
expect(result.success).toBe(true);
|
|
2228
|
+
expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
|
|
2229
|
+
mockProjectKey,
|
|
2230
|
+
mockPullRequestId,
|
|
2231
|
+
mockRepositorySlug,
|
|
2232
|
+
{ text: 'Task comment', severity: 'BLOCKER' }
|
|
2233
|
+
);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
it('should NOT include severity in the request body when severity is omitted', async () => {
|
|
2237
|
+
const mockComment = { id: 201, text: 'Normal comment', author: { displayName: 'Test User' } };
|
|
2238
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
2239
|
+
|
|
2240
|
+
await bitbucketService.postPullRequestComment(
|
|
2241
|
+
mockProjectKey,
|
|
2242
|
+
mockRepositorySlug,
|
|
2243
|
+
mockPullRequestId,
|
|
2244
|
+
'Normal comment'
|
|
2245
|
+
);
|
|
2246
|
+
|
|
2247
|
+
expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
|
|
2248
|
+
mockProjectKey,
|
|
2249
|
+
mockPullRequestId,
|
|
2250
|
+
mockRepositorySlug,
|
|
2251
|
+
{ text: 'Normal comment' } // no severity field
|
|
2252
|
+
);
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
it('should support severity BLOCKER combined with a file/line anchor', async () => {
|
|
2256
|
+
const mockComment = { id: 202, text: 'Inline task', author: { displayName: 'Test User' } };
|
|
2257
|
+
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
2258
|
+
|
|
2259
|
+
await bitbucketService.postPullRequestComment(
|
|
2260
|
+
mockProjectKey,
|
|
2261
|
+
mockRepositorySlug,
|
|
2262
|
+
mockPullRequestId,
|
|
2263
|
+
'Inline task',
|
|
2264
|
+
undefined, // parentId
|
|
2265
|
+
'src/index.ts', // filePath
|
|
2266
|
+
undefined, // startLine
|
|
2267
|
+
undefined, // startLineType
|
|
2268
|
+
10, // line
|
|
2269
|
+
'ADDED', // lineType
|
|
2270
|
+
undefined, // pending
|
|
2271
|
+
'BLOCKER' // severity
|
|
2272
|
+
);
|
|
2273
|
+
|
|
2274
|
+
expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
|
|
2275
|
+
mockProjectKey,
|
|
2276
|
+
mockPullRequestId,
|
|
2277
|
+
mockRepositorySlug,
|
|
2278
|
+
{
|
|
2279
|
+
text: 'Inline task',
|
|
2280
|
+
severity: 'BLOCKER',
|
|
2281
|
+
anchor: {
|
|
2282
|
+
path: 'src/index.ts',
|
|
2283
|
+
diffType: 'EFFECTIVE',
|
|
2284
|
+
line: 10,
|
|
2285
|
+
lineType: 'ADDED',
|
|
2286
|
+
fileType: 'TO'
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
);
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2005
2293
|
describe('postPullRequestComment - pending flag', () => {
|
|
2006
2294
|
it('should include state: PENDING in the request body when pending is true', async () => {
|
|
2007
2295
|
const mockComment = { id: 99, text: 'Draft comment', author: { displayName: 'Test User' } };
|
|
@@ -390,7 +390,7 @@ describe('BitbucketService token optimization paths', () => {
|
|
|
390
390
|
};
|
|
391
391
|
(PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
|
|
392
392
|
|
|
393
|
-
const result = await service.postPullRequestComment('TEST', 'repo', '123', 'Raw comment', undefined, undefined, undefined, undefined, undefined, undefined, undefined, 'full');
|
|
393
|
+
const result = await service.postPullRequestComment('TEST', 'repo', '123', 'Raw comment', undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 'full');
|
|
394
394
|
|
|
395
395
|
expect(result.success).toBe(true);
|
|
396
396
|
expect(result.data).toBe(mockComment);
|
|
@@ -113,10 +113,10 @@ export function shapePullRequestCommentAck(comment: any): Record<string, any> {
|
|
|
113
113
|
path: comment.anchor.path,
|
|
114
114
|
...(comment.anchor.line !== undefined ? { line: comment.anchor.line } : {}),
|
|
115
115
|
...(typeof comment.anchor.lineType === 'string' ? { lineType: comment.anchor.lineType } : {}),
|
|
116
|
-
...(comment.anchor.
|
|
116
|
+
...(comment.anchor.multilineMarker
|
|
117
117
|
? {
|
|
118
|
-
startLine: comment.anchor.
|
|
119
|
-
startLineType: comment.anchor.
|
|
118
|
+
startLine: comment.anchor.multilineMarker.startLine,
|
|
119
|
+
startLineType: comment.anchor.multilineMarker.startLineType,
|
|
120
120
|
}
|
|
121
121
|
: {}),
|
|
122
122
|
},
|
package/src/bitbucket-service.ts
CHANGED
|
@@ -23,6 +23,84 @@ function resolveToken(token: string | (() => string | undefined), missingTokenMe
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
type DiffLineType = 'ADDED' | 'REMOVED' | 'CONTEXT';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a Bitbucket DC inline comment anchor.
|
|
30
|
+
*
|
|
31
|
+
* For a multiline range, Bitbucket DC (>= 9.3.0) requires `multilineMarker` + `multilineSpan`.
|
|
32
|
+
* The legacy `multilineStartLine`/`multilineStartLineType`/`multilineAnchor`/`multilineDestinationRange`
|
|
33
|
+
* fields are silently ignored by the server, which then stores a single-line anchor — so a multiline
|
|
34
|
+
* ````suggestion` only replaces its first line on Apply. Field names verified against BB DC 9.3.2.
|
|
35
|
+
*/
|
|
36
|
+
function buildCommentAnchor(params: {
|
|
37
|
+
filePath: string;
|
|
38
|
+
line?: number;
|
|
39
|
+
lineType?: DiffLineType;
|
|
40
|
+
startLine?: number;
|
|
41
|
+
startLineType?: DiffLineType;
|
|
42
|
+
}): Record<string, any> {
|
|
43
|
+
const { filePath, line, lineType } = params;
|
|
44
|
+
const anchor: Record<string, any> = { path: filePath, diffType: 'EFFECTIVE' };
|
|
45
|
+
if (line === undefined || !lineType) {
|
|
46
|
+
return anchor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (params.startLine === undefined) {
|
|
50
|
+
anchor.line = line;
|
|
51
|
+
anchor.lineType = lineType;
|
|
52
|
+
anchor.fileType = lineType === 'REMOVED' ? 'FROM' : 'TO';
|
|
53
|
+
return anchor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Normalize so the marker sits on the lower line and `line` on the upper line.
|
|
57
|
+
let startLine = params.startLine;
|
|
58
|
+
let startLineType: DiffLineType = params.startLineType ?? lineType;
|
|
59
|
+
let endLine = line;
|
|
60
|
+
let endLineType: DiffLineType = lineType;
|
|
61
|
+
if (startLine > endLine) {
|
|
62
|
+
[startLine, endLine] = [endLine, startLine];
|
|
63
|
+
[startLineType, endLineType] = [endLineType, startLineType];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fileType = endLineType === 'REMOVED' ? 'FROM' : 'TO';
|
|
67
|
+
anchor.line = endLine;
|
|
68
|
+
anchor.lineType = endLineType;
|
|
69
|
+
anchor.fileType = fileType;
|
|
70
|
+
anchor.multilineMarker = { startLine, startLineType };
|
|
71
|
+
anchor.multilineSpan =
|
|
72
|
+
fileType === 'FROM'
|
|
73
|
+
? { srcSpanStart: startLine, srcSpanEnd: endLine }
|
|
74
|
+
: { dstSpanStart: startLine, dstSpanEnd: endLine };
|
|
75
|
+
return anchor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const SUGGESTION_BLOCK_RE = /```suggestion\b[^\n]*\n([\s\S]*?)```/;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Warn when a multi-line ````suggestion` block is anchored to a single line: Bitbucket only replaces
|
|
82
|
+
* the anchored line on Apply, leaving the rest (duplicated code). Returns undefined when there is no
|
|
83
|
+
* concern. Heuristic — a 1-line anchor with a multi-line suggestion is the common failure shape.
|
|
84
|
+
*/
|
|
85
|
+
function multilineSuggestionWarning(text: string, startLine?: number): string | undefined {
|
|
86
|
+
if (startLine !== undefined) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const match = SUGGESTION_BLOCK_RE.exec(text);
|
|
90
|
+
if (!match) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const suggestionLineCount = match[1].replace(/\n$/, '').split('\n').length;
|
|
94
|
+
if (suggestionLineCount <= 1) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
'The comment contains a multi-line ```suggestion block but is anchored to a single line. ' +
|
|
99
|
+
'On Apply, Bitbucket replaces only that one line and leaves the rest. To replace multiple lines, ' +
|
|
100
|
+
'set startLine (first replaced line) and line (last replaced line) to span the whole range.'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
26
104
|
export class BitbucketService {
|
|
27
105
|
private readonly getPageSize: () => number;
|
|
28
106
|
|
|
@@ -301,13 +379,14 @@ export class BitbucketService {
|
|
|
301
379
|
* @param text The comment text
|
|
302
380
|
* @param parentId Optional parent comment ID for replies
|
|
303
381
|
* @param filePath Optional file path for file-specific comments
|
|
304
|
-
* @param line Optional end line number for line/multiline comments
|
|
305
|
-
* @param lineType Optional line type ('ADDED', 'REMOVED', 'CONTEXT') for the end line
|
|
306
382
|
* @param startLine Optional start line for multiline comments; when provided together with line, creates a range comment
|
|
307
383
|
* @param startLineType Optional line type for the start line; defaults to lineType if omitted
|
|
384
|
+
* @param line Optional end line number for line/multiline comments
|
|
385
|
+
* @param lineType Optional line type ('ADDED', 'REMOVED', 'CONTEXT') for the end line
|
|
308
386
|
* @param pending Optional flag to create a pending (draft) comment, not visible to others until a review is submitted.
|
|
309
387
|
* Only works when filePath is provided (file-level or inline comments).
|
|
310
388
|
* Top-level PR comments (no filePath) are always posted live regardless of this flag.
|
|
389
|
+
* @param severity Optional severity for the comment. 'BLOCKER' posts the comment as a task that must be resolved before the PR can be merged. Defaults to 'NORMAL' (regular comment) when omitted.
|
|
311
390
|
* @returns Promise with created comment data
|
|
312
391
|
*/
|
|
313
392
|
async postPullRequestComment(
|
|
@@ -322,6 +401,7 @@ export class BitbucketService {
|
|
|
322
401
|
line?: number,
|
|
323
402
|
lineType?: 'ADDED' | 'REMOVED' | 'CONTEXT',
|
|
324
403
|
pending?: boolean,
|
|
404
|
+
severity?: 'NORMAL' | 'BLOCKER',
|
|
325
405
|
output: BitbucketMutationOutputMode = 'ack'
|
|
326
406
|
) {
|
|
327
407
|
projectKey = projectKey.toUpperCase();
|
|
@@ -330,6 +410,10 @@ export class BitbucketService {
|
|
|
330
410
|
text
|
|
331
411
|
};
|
|
332
412
|
|
|
413
|
+
if (severity) {
|
|
414
|
+
comment.severity = severity;
|
|
415
|
+
}
|
|
416
|
+
|
|
333
417
|
// Mark comment as pending (draft) — not visible to others until review is submitted
|
|
334
418
|
// Note: Bitbucket DC uses state:'PENDING' not pending:true
|
|
335
419
|
if (pending) {
|
|
@@ -343,27 +427,11 @@ export class BitbucketService {
|
|
|
343
427
|
|
|
344
428
|
// Add anchor for file/line comments
|
|
345
429
|
if (filePath) {
|
|
346
|
-
comment.anchor = {
|
|
347
|
-
path: filePath,
|
|
348
|
-
diffType: 'EFFECTIVE'
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Add line-specific anchor properties
|
|
352
|
-
if (line !== undefined && lineType) {
|
|
353
|
-
comment.anchor.line = line;
|
|
354
|
-
comment.anchor.lineType = lineType;
|
|
355
|
-
comment.anchor.fileType = 'TO'; // Default to destination file
|
|
356
|
-
|
|
357
|
-
if (startLine !== undefined) {
|
|
358
|
-
const resolvedStartLineType = startLineType ?? lineType;
|
|
359
|
-
comment.anchor.multilineAnchor = true;
|
|
360
|
-
comment.anchor.multilineStartLine = startLine;
|
|
361
|
-
comment.anchor.multilineStartLineType = resolvedStartLineType;
|
|
362
|
-
comment.anchor.multilineDestinationRange = { minimum: startLine, maximum: line };
|
|
363
|
-
}
|
|
364
|
-
}
|
|
430
|
+
comment.anchor = buildCommentAnchor({ filePath, line, lineType, startLine, startLineType });
|
|
365
431
|
}
|
|
366
432
|
|
|
433
|
+
const suggestionWarning = filePath ? multilineSuggestionWarning(text, startLine) : undefined;
|
|
434
|
+
|
|
367
435
|
const result = await handleApiOperation(
|
|
368
436
|
() => PullRequestsService.createComment2(
|
|
369
437
|
projectKey,
|
|
@@ -374,6 +442,76 @@ export class BitbucketService {
|
|
|
374
442
|
'Error posting pull request comment'
|
|
375
443
|
);
|
|
376
444
|
|
|
445
|
+
if (result.success && result.data && output !== 'full') {
|
|
446
|
+
return {
|
|
447
|
+
...result,
|
|
448
|
+
data: {
|
|
449
|
+
...shapePullRequestCommentAck(result.data),
|
|
450
|
+
...(suggestionWarning ? { warning: suggestionWarning } : {}),
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (result.success && suggestionWarning) {
|
|
456
|
+
return { ...result, warning: suggestionWarning };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Update an existing pull request comment. Use this to edit text, change severity, or change state.
|
|
464
|
+
* On a BLOCKER (task) comment, setting state to 'RESOLVED' ticks the task. Note: this is NOT the same
|
|
465
|
+
* as the thread-level "Resolve" button on regular comment threads — that operates on CommentThread.resolved
|
|
466
|
+
* and is a separate concept not exposed by this endpoint.
|
|
467
|
+
* @param projectKey The project key
|
|
468
|
+
* @param repositorySlug The repository slug
|
|
469
|
+
* @param pullRequestId The pull request ID
|
|
470
|
+
* @param commentId The comment ID to update
|
|
471
|
+
* @param version The current version of the comment (required for optimistic locking)
|
|
472
|
+
* @param text Optional new comment text
|
|
473
|
+
* @param state Optional new state. On a BLOCKER comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it.
|
|
474
|
+
* @param severity Optional new severity. 'BLOCKER' converts a comment into a task, 'NORMAL' converts a task back to a regular comment.
|
|
475
|
+
* @returns Promise with updated comment data
|
|
476
|
+
*/
|
|
477
|
+
async updatePullRequestComment(
|
|
478
|
+
projectKey: string,
|
|
479
|
+
repositorySlug: string,
|
|
480
|
+
pullRequestId: string,
|
|
481
|
+
commentId: string,
|
|
482
|
+
version: number,
|
|
483
|
+
text?: string,
|
|
484
|
+
state?: 'OPEN' | 'RESOLVED',
|
|
485
|
+
severity?: 'NORMAL' | 'BLOCKER',
|
|
486
|
+
output: BitbucketMutationOutputMode = 'ack'
|
|
487
|
+
) {
|
|
488
|
+
projectKey = projectKey.toUpperCase();
|
|
489
|
+
repositorySlug = repositorySlug.toLowerCase();
|
|
490
|
+
const comment: any = { version };
|
|
491
|
+
|
|
492
|
+
if (text !== undefined) {
|
|
493
|
+
comment.text = text;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (state) {
|
|
497
|
+
comment.state = state;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (severity) {
|
|
501
|
+
comment.severity = severity;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const result = await handleApiOperation(
|
|
505
|
+
() => PullRequestsService.updateComment2(
|
|
506
|
+
projectKey,
|
|
507
|
+
commentId,
|
|
508
|
+
pullRequestId,
|
|
509
|
+
repositorySlug,
|
|
510
|
+
comment
|
|
511
|
+
),
|
|
512
|
+
'Error updating pull request comment'
|
|
513
|
+
);
|
|
514
|
+
|
|
377
515
|
if (result.success && result.data && output !== 'full') {
|
|
378
516
|
return {
|
|
379
517
|
...result,
|
|
@@ -842,11 +980,23 @@ export const bitbucketToolSchemas = {
|
|
|
842
980
|
text: z.string().describe("The comment text"),
|
|
843
981
|
parentId: z.number().optional().describe("Parent comment ID for replies"),
|
|
844
982
|
filePath: z.string().optional().describe("File path for file-specific comments"),
|
|
845
|
-
startLine: z.number().optional().describe("
|
|
846
|
-
startLineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for the start line of a multiline
|
|
847
|
-
line: z.number().optional().describe("
|
|
848
|
-
lineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for the end line
|
|
983
|
+
startLine: z.number().optional().describe("First line of a multiline range. Provide together with 'line' (the last line) to span multiple lines. REQUIRED for a multi-line ```suggestion: the anchored range (startLine..line) is exactly what 'Apply suggestion' replaces — omit it and only the single 'line' is replaced, leaving the rest. Requires Bitbucket DC >= 9.3.0 for multiline suggestions."),
|
|
984
|
+
startLineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for the start line of a multiline range. Defaults to the same value as lineType if omitted."),
|
|
985
|
+
line: z.number().optional().describe("Single-line comments: the line. Multiline: the LAST line of the range (use startLine for the first). For a ```suggestion this is the last replaced line."),
|
|
986
|
+
lineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for 'line' (the end line, or the only line for single-line comments). Use 'ADDED'/'CONTEXT' for the new/target file, 'REMOVED' for the original/source file."),
|
|
849
987
|
pending: z.boolean().optional().describe("If true, creates a pending (draft) comment not visible to others until the review is submitted via bitbucket_submitPullRequestReview. Only works when filePath is provided — top-level PR comments (no filePath) are always posted live."),
|
|
988
|
+
severity: z.enum(['NORMAL', 'BLOCKER']).optional().describe("Comment severity. Use 'BLOCKER' to post the comment as a task that must be resolved before the PR can be merged. Defaults to 'NORMAL' (regular comment)."),
|
|
989
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
990
|
+
},
|
|
991
|
+
updatePullRequestComment: {
|
|
992
|
+
projectKey: z.string().describe("The project key"),
|
|
993
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
994
|
+
pullRequestId: z.string().describe("The pull request ID"),
|
|
995
|
+
commentId: z.string().describe("The ID of the comment to update"),
|
|
996
|
+
version: z.number().describe("The current version of the comment, required for optimistic locking. Get it from bitbucket_getPR_CommentsAndAction or from the response of the original post/update."),
|
|
997
|
+
text: z.string().optional().describe("New comment text. Omit to leave unchanged."),
|
|
998
|
+
state: z.enum(['OPEN', 'RESOLVED']).optional().describe("New state. On a BLOCKER (task) comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it. This is NOT the thread-level 'Resolve' button on regular comment threads — that is a separate concept (CommentThread.resolved) and is not exposed by this endpoint."),
|
|
999
|
+
severity: z.enum(['NORMAL', 'BLOCKER']).optional().describe("New severity. Use 'BLOCKER' to convert a comment into a task, 'NORMAL' to convert it back."),
|
|
850
1000
|
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
851
1001
|
},
|
|
852
1002
|
getUser: {
|
package/src/index.ts
CHANGED
|
@@ -129,10 +129,20 @@ server.tool(
|
|
|
129
129
|
|
|
130
130
|
server.tool(
|
|
131
131
|
"bitbucket_postPullRequestComment",
|
|
132
|
-
"Post a comment to a Bitbucket pull request. Use pending: true to create a draft comment that is only visible to you until you call bitbucket_submitPullRequestReview. NOTE: pending only works when filePath is provided (file-level or inline comments). True top-level PR comments (no filePath) are always posted live and cannot be drafted.",
|
|
132
|
+
"Post a comment to a Bitbucket pull request. Use pending: true to create a draft comment that is only visible to you until you call bitbucket_submitPullRequestReview. NOTE: pending only works when filePath is provided (file-level or inline comments). True top-level PR comments (no filePath) are always posted live and cannot be drafted. Use severity: 'BLOCKER' to post the comment as a task — tasks must be resolved before the PR can be merged.",
|
|
133
133
|
bitbucketToolSchemas.postPullRequestComment,
|
|
134
|
-
async ({ projectKey, repositorySlug, pullRequestId, text, parentId, filePath, startLine, startLineType, line, lineType, pending, output }) => {
|
|
135
|
-
const result = await bitbucketService.postPullRequestComment(projectKey, repositorySlug, pullRequestId, text, parentId, filePath, startLine, startLineType, line, lineType, pending, output);
|
|
134
|
+
async ({ projectKey, repositorySlug, pullRequestId, text, parentId, filePath, startLine, startLineType, line, lineType, pending, severity, output }) => {
|
|
135
|
+
const result = await bitbucketService.postPullRequestComment(projectKey, repositorySlug, pullRequestId, text, parentId, filePath, startLine, startLineType, line, lineType, pending, severity, output);
|
|
136
|
+
return formatToolResponse(result);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
server.tool(
|
|
141
|
+
"bitbucket_updatePullRequestComment",
|
|
142
|
+
"Update an existing pull request comment. Use to edit text, change severity, or change state. On a BLOCKER (task) comment, state: 'RESOLVED' ticks the task. NOTE: this is the task-tick, not the thread-level 'Resolve' button on regular comment threads — that is a separate concept (CommentThread.resolved) and is not exposed by this endpoint. Requires the current 'version' from optimistic locking; fetch it via bitbucket_getPR_CommentsAndAction or use the version returned when the comment was created.",
|
|
143
|
+
bitbucketToolSchemas.updatePullRequestComment,
|
|
144
|
+
async ({ projectKey, repositorySlug, pullRequestId, commentId, version, text, state, severity, output }) => {
|
|
145
|
+
const result = await bitbucketService.updatePullRequestComment(projectKey, repositorySlug, pullRequestId, commentId, version, text, state, severity, output);
|
|
136
146
|
return formatToolResponse(result);
|
|
137
147
|
}
|
|
138
148
|
);
|