@achieveai/azuredevops-mcp 1.3.17 → 1.3.19

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.
Files changed (119) hide show
  1. package/README.md +76 -0
  2. package/dist/Interfaces/Common.js +37 -1
  3. package/dist/Interfaces/Common.js.map +1 -1
  4. package/dist/Services/AzureDevOpsService.js +234 -32
  5. package/dist/Services/AzureDevOpsService.js.map +1 -1
  6. package/dist/Services/BoardsSprintsService.js +111 -13
  7. package/dist/Services/BoardsSprintsService.js.map +1 -1
  8. package/dist/Services/BuildService.js +157 -24
  9. package/dist/Services/BuildService.js.map +1 -1
  10. package/dist/Services/GitService.js +26 -3
  11. package/dist/Services/GitService.js.map +1 -1
  12. package/dist/Services/ProjectService.js +47 -6
  13. package/dist/Services/ProjectService.js.map +1 -1
  14. package/dist/Services/WorkItemService.js +183 -170
  15. package/dist/Services/WorkItemService.js.map +1 -1
  16. package/dist/Tools/BoardsSprintsTools.js +2 -8
  17. package/dist/Tools/BoardsSprintsTools.js.map +1 -1
  18. package/dist/Tools/BuildTools.js +5 -8
  19. package/dist/Tools/BuildTools.js.map +1 -1
  20. package/dist/Tools/GitTools.js +177 -62
  21. package/dist/Tools/GitTools.js.map +1 -1
  22. package/dist/Tools/WorkItemTools.js +110 -172
  23. package/dist/Tools/WorkItemTools.js.map +1 -1
  24. package/dist/index.js +31 -26
  25. package/dist/index.js.map +1 -1
  26. package/dist/utils/apiUsageGuidance.js +336 -0
  27. package/dist/utils/apiUsageGuidance.js.map +1 -0
  28. package/dist/utils/formatHelpers.js +15 -0
  29. package/dist/utils/formatHelpers.js.map +1 -1
  30. package/package.json +3 -3
  31. package/dist/Services/BuildService.project.test.js +0 -91
  32. package/dist/Services/BuildService.project.test.js.map +0 -1
  33. package/dist/Services/GitService.project.test.js +0 -407
  34. package/dist/Services/GitService.project.test.js.map +0 -1
  35. package/dist/package.json +0 -59
  36. package/dist/src/Interfaces/AIAssisted.js +0 -3
  37. package/dist/src/Interfaces/AIAssisted.js.map +0 -1
  38. package/dist/src/Interfaces/ArtifactManagement.js +0 -3
  39. package/dist/src/Interfaces/ArtifactManagement.js.map +0 -1
  40. package/dist/src/Interfaces/AzureDevOps.js +0 -3
  41. package/dist/src/Interfaces/AzureDevOps.js.map +0 -1
  42. package/dist/src/Interfaces/BoardsAndSprints.js +0 -3
  43. package/dist/src/Interfaces/BoardsAndSprints.js.map +0 -1
  44. package/dist/src/Interfaces/CodeAndRepositories.js +0 -3
  45. package/dist/src/Interfaces/CodeAndRepositories.js.map +0 -1
  46. package/dist/src/Interfaces/Common.js +0 -134
  47. package/dist/src/Interfaces/Common.js.map +0 -1
  48. package/dist/src/Interfaces/CostResourceManagement.js +0 -3
  49. package/dist/src/Interfaces/CostResourceManagement.js.map +0 -1
  50. package/dist/src/Interfaces/DevSecOps.js +0 -3
  51. package/dist/src/Interfaces/DevSecOps.js.map +0 -1
  52. package/dist/src/Interfaces/ExternalIntegrations.js +0 -3
  53. package/dist/src/Interfaces/ExternalIntegrations.js.map +0 -1
  54. package/dist/src/Interfaces/HybridCrossPlatform.js +0 -3
  55. package/dist/src/Interfaces/HybridCrossPlatform.js.map +0 -1
  56. package/dist/src/Interfaces/Pipelines.js +0 -3
  57. package/dist/src/Interfaces/Pipelines.js.map +0 -1
  58. package/dist/src/Interfaces/ProjectManagement.js +0 -3
  59. package/dist/src/Interfaces/ProjectManagement.js.map +0 -1
  60. package/dist/src/Interfaces/TestingCapabilities.js +0 -3
  61. package/dist/src/Interfaces/TestingCapabilities.js.map +0 -1
  62. package/dist/src/Interfaces/Wiki.js +0 -3
  63. package/dist/src/Interfaces/Wiki.js.map +0 -1
  64. package/dist/src/Interfaces/WorkItems.js +0 -3
  65. package/dist/src/Interfaces/WorkItems.js.map +0 -1
  66. package/dist/src/Services/AIAssistedDevelopmentService.js +0 -195
  67. package/dist/src/Services/AIAssistedDevelopmentService.js.map +0 -1
  68. package/dist/src/Services/ArtifactManagementService.js +0 -346
  69. package/dist/src/Services/ArtifactManagementService.js.map +0 -1
  70. package/dist/src/Services/AzureDevOpsService.js +0 -385
  71. package/dist/src/Services/AzureDevOpsService.js.map +0 -1
  72. package/dist/src/Services/BoardsSprintsService.js +0 -339
  73. package/dist/src/Services/BoardsSprintsService.js.map +0 -1
  74. package/dist/src/Services/BuildService.js +0 -405
  75. package/dist/src/Services/BuildService.js.map +0 -1
  76. package/dist/src/Services/DevSecOpsService.js +0 -307
  77. package/dist/src/Services/DevSecOpsService.js.map +0 -1
  78. package/dist/src/Services/EntraAuthHandler.js +0 -337
  79. package/dist/src/Services/EntraAuthHandler.js.map +0 -1
  80. package/dist/src/Services/GitService.js +0 -1595
  81. package/dist/src/Services/GitService.js.map +0 -1
  82. package/dist/src/Services/ProjectService.js +0 -257
  83. package/dist/src/Services/ProjectService.js.map +0 -1
  84. package/dist/src/Services/TestingCapabilitiesService.js +0 -149
  85. package/dist/src/Services/TestingCapabilitiesService.js.map +0 -1
  86. package/dist/src/Services/WikiService.js +0 -90
  87. package/dist/src/Services/WikiService.js.map +0 -1
  88. package/dist/src/Services/WorkItemService.js +0 -885
  89. package/dist/src/Services/WorkItemService.js.map +0 -1
  90. package/dist/src/Tools/AIAssistedDevelopmentTools.js +0 -137
  91. package/dist/src/Tools/AIAssistedDevelopmentTools.js.map +0 -1
  92. package/dist/src/Tools/ArtifactManagementTools.js +0 -140
  93. package/dist/src/Tools/ArtifactManagementTools.js.map +0 -1
  94. package/dist/src/Tools/BoardsSprintsTools.js +0 -338
  95. package/dist/src/Tools/BoardsSprintsTools.js.map +0 -1
  96. package/dist/src/Tools/BuildTools.js +0 -468
  97. package/dist/src/Tools/BuildTools.js.map +0 -1
  98. package/dist/src/Tools/DevSecOpsTools.js +0 -147
  99. package/dist/src/Tools/DevSecOpsTools.js.map +0 -1
  100. package/dist/src/Tools/GitTools.js +0 -1475
  101. package/dist/src/Tools/GitTools.js.map +0 -1
  102. package/dist/src/Tools/ProjectTools.js +0 -360
  103. package/dist/src/Tools/ProjectTools.js.map +0 -1
  104. package/dist/src/Tools/TestingCapabilitiesTools.js +0 -157
  105. package/dist/src/Tools/TestingCapabilitiesTools.js.map +0 -1
  106. package/dist/src/Tools/WikiTools.js +0 -137
  107. package/dist/src/Tools/WikiTools.js.map +0 -1
  108. package/dist/src/Tools/WorkItemTools.js +0 -862
  109. package/dist/src/Tools/WorkItemTools.js.map +0 -1
  110. package/dist/src/config.js +0 -176
  111. package/dist/src/config.js.map +0 -1
  112. package/dist/src/index.js +0 -1716
  113. package/dist/src/index.js.map +0 -1
  114. package/dist/src/utils/formatHelpers.js +0 -257
  115. package/dist/src/utils/formatHelpers.js.map +0 -1
  116. package/dist/src/utils/getClassMethods.js +0 -8
  117. package/dist/src/utils/getClassMethods.js.map +0 -1
  118. package/dist/src/utils/repositoryResolver.js +0 -40
  119. package/dist/src/utils/repositoryResolver.js.map +0 -1
@@ -1,1595 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GitService = void 0;
4
- const GitInterfaces_1 = require("azure-devops-node-api/interfaces/GitInterfaces");
5
- const AzureDevOpsService_1 = require("./AzureDevOpsService");
6
- const repositoryResolver_1 = require("../utils/repositoryResolver");
7
- const formatHelpers_1 = require("../utils/formatHelpers");
8
- class GitService extends AzureDevOpsService_1.AzureDevOpsService {
9
- constructor(config) {
10
- super(config);
11
- // Cache for repository name-to-ID mappings to improve performance
12
- this.repositoryCache = new Map();
13
- }
14
- /**
15
- * Get the Git API client
16
- */
17
- async getGitApi() {
18
- return await this.connection.getGitApi();
19
- }
20
- /**
21
- * Get work item references linked to a pull request
22
- */
23
- async getPullRequestWorkItemRefs(repository, pullRequestId, project) {
24
- const gitApi = await this.getGitApi();
25
- const effectiveProject = project || this.config.project;
26
- const repositoryId = await this.resolveRepositoryId(repository, effectiveProject);
27
- const refs = await gitApi.getPullRequestWorkItemRefs(repositoryId, pullRequestId, effectiveProject);
28
- if (!refs || refs.length === 0)
29
- return [];
30
- return refs.map(ref => {
31
- const match = ref.url?.match(/workitems?\/(\d+)/i);
32
- return { id: match ? parseInt(match[1]) : 0, url: ref.url };
33
- }).filter(r => r.id > 0);
34
- }
35
- /**
36
- * Resolve repository identifier (name or ID) to repository ID
37
- * Supports hybrid approach: if input is already a GUID, returns it; if name, resolves to ID
38
- * @param repository - Repository name or ID
39
- * @param projectId - Optional project ID for scoping (defaults to config project)
40
- * @returns Repository ID (GUID)
41
- */
42
- async resolveRepositoryId(repository, projectId) {
43
- // If it's already a GUID, return as-is
44
- if ((0, repositoryResolver_1.isRepositoryId)(repository)) {
45
- return repository;
46
- }
47
- // Check cache first for performance
48
- const cacheKey = `${projectId || this.config.project}:${repository}`;
49
- if (this.repositoryCache.has(cacheKey)) {
50
- return this.repositoryCache.get(cacheKey);
51
- }
52
- try {
53
- // Resolve repository name to ID by listing repositories
54
- const repositories = await this.listRepositories({
55
- projectId: projectId || this.config.project
56
- });
57
- // Find repository by name (case-insensitive)
58
- const matchedRepo = repositories.find((repo) => repo.name && repo.name.toLowerCase() === repository.toLowerCase());
59
- if (!matchedRepo) {
60
- throw new Error(`Repository '${repository}' not found in project '${projectId || this.config.project}'. ` +
61
- `Available repositories: ${repositories.map((r) => r.name).join(', ')}`);
62
- }
63
- // Cache the result
64
- this.repositoryCache.set(cacheKey, matchedRepo.id);
65
- return matchedRepo.id;
66
- }
67
- catch (error) {
68
- if (error instanceof Error && error.message.includes('not found')) {
69
- throw error; // Re-throw our custom error
70
- }
71
- console.error(`Error resolving repository '${repository}':`, error);
72
- throw new Error(`Failed to resolve repository '${repository}'. ` +
73
- `Please verify the repository name exists and you have access to it.`);
74
- }
75
- }
76
- /**
77
- * List all repositories
78
- */
79
- async listRepositories(params) {
80
- try {
81
- const gitApi = await this.getGitApi();
82
- const repositories = await this.withAuthRetry(() => gitApi.getRepositories(params.projectId || this.config.project, params.includeHidden, params.includeAllUrls), {
83
- operationName: 'git.repositories.list',
84
- details: {
85
- projectId: params.projectId || this.config.project,
86
- includeHidden: params.includeHidden,
87
- includeAllUrls: params.includeAllUrls,
88
- },
89
- });
90
- return repositories;
91
- }
92
- catch (error) {
93
- console.error('Error listing repositories:', error);
94
- throw error;
95
- }
96
- }
97
- /**
98
- * Get repository details
99
- */
100
- async getRepository(params) {
101
- try {
102
- const gitApi = await this.getGitApi();
103
- // Resolve repository name/ID to actual repository ID
104
- const repositoryId = await this.resolveRepositoryId(params.repository, params.projectId);
105
- const repository = await gitApi.getRepository(repositoryId, params.projectId || this.config.project);
106
- return repository;
107
- }
108
- catch (error) {
109
- console.error(`Error getting repository ${params.repository}:`, error);
110
- throw error;
111
- }
112
- }
113
- /**
114
- * Create a repository
115
- */
116
- async createRepository(params) {
117
- try {
118
- const gitApi = await this.getGitApi();
119
- const repository = await gitApi.createRepository({
120
- name: params.name,
121
- project: {
122
- id: params.projectId || this.config.project
123
- }
124
- }, params.projectId || this.config.project);
125
- return repository;
126
- }
127
- catch (error) {
128
- console.error(`Error creating repository ${params.name}:`, error);
129
- throw error;
130
- }
131
- }
132
- /**
133
- * List branches
134
- */
135
- async listBranches(params) {
136
- try {
137
- const gitApi = await this.getGitApi();
138
- const project = params.project || this.config.project;
139
- // Resolve repository name/ID to actual repository ID
140
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
141
- const branches = await gitApi.getBranches(repositoryId, params.filter);
142
- if (params.top && branches.length > params.top) {
143
- return branches.slice(0, params.top);
144
- }
145
- return branches;
146
- }
147
- catch (error) {
148
- console.error(`Error listing branches for repository ${params.repository}:`, error);
149
- throw error;
150
- }
151
- }
152
- /**
153
- * Search code (Note: This uses a simplified approach as the full-text search API
154
- * might require additional setup)
155
- */
156
- async searchCode(params) {
157
- try {
158
- const gitApi = await this.getGitApi();
159
- const project = params.projectId || this.config.project;
160
- // Resolve repository name/ID to actual repository ID if provided
161
- const repositoryId = params.repository ? await this.resolveRepositoryId(params.repository, project) : "";
162
- // This is a simplified implementation using item search
163
- // For more comprehensive code search, you'd use the Search API
164
- const items = await gitApi.getItems(repositoryId || "", undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined);
165
- // Simple filter based on the search text and file extension
166
- let filteredItems = items;
167
- if (params.searchText) {
168
- filteredItems = filteredItems.filter(item => item.path && item.path.toLowerCase().includes(params.searchText.toLowerCase()));
169
- }
170
- if (params.fileExtension) {
171
- filteredItems = filteredItems.filter(item => item.path && item.path.endsWith(params.fileExtension || ""));
172
- }
173
- // Limit results if top is specified
174
- if (params.top && filteredItems.length > params.top) {
175
- filteredItems = filteredItems.slice(0, params.top);
176
- }
177
- return filteredItems;
178
- }
179
- catch (error) {
180
- console.error(`Error searching code in repository ${params.repository}:`, error);
181
- throw error;
182
- }
183
- }
184
- /**
185
- * Browse repository
186
- */
187
- async browseRepository(params) {
188
- try {
189
- const gitApi = await this.getGitApi();
190
- const project = params.project || this.config.project;
191
- // Resolve repository name/ID to actual repository ID
192
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
193
- const items = await gitApi.getItems(repositoryId, undefined, params.path, undefined, true, undefined, undefined, undefined, undefined, undefined);
194
- return items;
195
- }
196
- catch (error) {
197
- console.error(`Error browsing repository ${params.repository}:`, error);
198
- throw error;
199
- }
200
- }
201
- /**
202
- * Get file content
203
- */
204
- async getFileContent(params) {
205
- try {
206
- const gitApi = await this.getGitApi();
207
- const project = params.project || this.config.project;
208
- // Resolve repository name/ID to actual repository ID
209
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
210
- // Get the file content as a stream
211
- const content = await gitApi.getItemContent(repositoryId, params.path, undefined, undefined);
212
- let fileContent = '';
213
- // Handle different content types
214
- if (Buffer.isBuffer(content)) {
215
- fileContent = content.toString('utf8');
216
- }
217
- else if (typeof content === 'string') {
218
- fileContent = content;
219
- }
220
- else if (content && typeof content === 'object' && 'pipe' in content && typeof content.pipe === 'function') {
221
- // Handle stream content
222
- const chunks = [];
223
- const stream = content;
224
- fileContent = await new Promise((resolve, reject) => {
225
- const timeout = setTimeout(() => {
226
- stream.destroy();
227
- reject(new Error(`Stream timeout for ${params.path}`));
228
- }, 30000);
229
- stream.on('data', (chunk) => {
230
- chunks.push(chunk);
231
- });
232
- stream.on('end', () => {
233
- clearTimeout(timeout);
234
- const buffer = Buffer.concat(chunks);
235
- resolve(buffer.toString('utf8'));
236
- });
237
- stream.on('error', (error) => {
238
- clearTimeout(timeout);
239
- console.error(`Error reading stream for ${params.path}:`, error);
240
- reject(error);
241
- });
242
- });
243
- }
244
- else {
245
- // If it's some other type, return a placeholder
246
- fileContent = "[Content not available in this format]";
247
- }
248
- // Process line range if specified
249
- const lines = fileContent.split('\n');
250
- const totalLines = lines.length;
251
- // Calculate effective start line (1-based, default to 1)
252
- const startLine = Math.max(1, params.startLine || 1);
253
- // Calculate effective line count (default to all, max 200)
254
- const requestedLineCount = params.lineCount || totalLines;
255
- const maxLineCount = Math.min(requestedLineCount, 200);
256
- // Calculate effective end line
257
- const endLine = Math.min(startLine + maxLineCount - 1, totalLines);
258
- // Extract the requested range (convert to 0-based for array slicing)
259
- const requestedLines = lines.slice(startLine - 1, endLine);
260
- const slicedContent = requestedLines.join('\n');
261
- return {
262
- content: slicedContent,
263
- metadata: {
264
- startLine: startLine,
265
- endLine: endLine,
266
- totalLines: totalLines,
267
- requestedStartLine: params.startLine || 1,
268
- requestedLineCount: requestedLineCount,
269
- actualLineCount: requestedLines.length
270
- }
271
- };
272
- }
273
- catch (error) {
274
- console.error(`Error getting file content for ${params.path}:`, error);
275
- throw error;
276
- }
277
- }
278
- /**
279
- * Get file content by object ID
280
- */
281
- async getFileContentByObjectId(repositoryId, objectId, project) {
282
- try {
283
- const gitApi = await this.getGitApi();
284
- // Get content by blob ID (object ID)
285
- const content = await gitApi.getBlobContent(repositoryId, objectId, project || this.config.project);
286
- if (Buffer.isBuffer(content)) {
287
- return content.toString('utf8');
288
- }
289
- else if (typeof content === 'string') {
290
- return content;
291
- }
292
- else if (content && typeof content === 'object' && 'pipe' in content && typeof content.pipe === 'function') {
293
- // Handle stream content
294
- const chunks = [];
295
- const stream = content;
296
- return new Promise((resolve, reject) => {
297
- const timeout = setTimeout(() => {
298
- stream.destroy();
299
- reject(new Error(`Stream timeout for objectId ${objectId}`));
300
- }, 30000);
301
- stream.on('data', (chunk) => {
302
- chunks.push(chunk);
303
- });
304
- stream.on('end', () => {
305
- clearTimeout(timeout);
306
- const buffer = Buffer.concat(chunks);
307
- resolve(buffer.toString('utf8'));
308
- });
309
- stream.on('error', (error) => {
310
- clearTimeout(timeout);
311
- reject(error);
312
- });
313
- });
314
- }
315
- return '[Content not available]';
316
- }
317
- catch (error) {
318
- console.error(`Error getting file content by objectId ${objectId}:`, error);
319
- return '[Content not available]';
320
- }
321
- }
322
- /**
323
- * Get the latest iteration number for a pull request
324
- * Returns the most recent iteration that contains the latest changes
325
- */
326
- async getLatestPullRequestIteration(repositoryId, pullRequestId, project) {
327
- try {
328
- const gitApi = await this.getGitApi();
329
- // Get all iterations for the pull request
330
- const iterations = await gitApi.getPullRequestIterations(repositoryId, pullRequestId, project || this.config.project);
331
- if (!iterations || iterations.length === 0) {
332
- // Fallback to iteration 1 if no iterations found
333
- return 1;
334
- }
335
- // Return the latest iteration number
336
- const latestIteration = Math.max(...iterations.map(i => i.id || 1));
337
- return latestIteration;
338
- }
339
- catch (error) {
340
- console.error(`Error getting latest iteration for PR ${pullRequestId}:`, error);
341
- // Fallback to iteration 1 if there's an error
342
- return 1;
343
- }
344
- }
345
- /**
346
- * Calculate enhanced unified diff between two file contents with better readability
347
- */
348
- calculateUnifiedDiff(originalContent, currentContent, filePath) {
349
- const originalLines = originalContent.split('\n');
350
- const currentLines = currentContent.split('\n');
351
- // Enhanced diff algorithm with better change grouping
352
- let diffLines = [];
353
- diffLines.push(`--- a${filePath}`);
354
- diffLines.push(`+++ b${filePath}`);
355
- // Use a more sophisticated approach to find meaningful changes
356
- const changes = this.findMeaningfulChanges(originalLines, currentLines);
357
- if (changes.length === 0) {
358
- diffLines.push(`@@ -1,${originalLines.length} +1,${currentLines.length} @@`);
359
- diffLines.push(' (No meaningful differences found - likely formatting changes)');
360
- return diffLines.join('\n');
361
- }
362
- // Group changes into hunks with context
363
- for (const change of changes) {
364
- const contextLines = 3;
365
- const hunkStart = Math.max(1, change.originalStart - contextLines);
366
- const hunkEnd = Math.min(originalLines.length, change.originalEnd + contextLines);
367
- // Add hunk header
368
- const originalHunkSize = change.originalEnd - change.originalStart + 1;
369
- const currentHunkSize = change.currentEnd - change.currentStart + 1;
370
- const hunkHeader = `@@ -${change.originalStart},${originalHunkSize} +${change.currentStart},${currentHunkSize} @@`;
371
- diffLines.push(hunkHeader);
372
- // Add context before
373
- for (let i = hunkStart - 1; i < change.originalStart - 1; i++) {
374
- if (i >= 0 && i < originalLines.length) {
375
- diffLines.push(` ${originalLines[i]}`);
376
- }
377
- }
378
- // Add the actual changes
379
- switch (change.type) {
380
- case 'modified':
381
- // Show removed lines
382
- for (let i = change.originalStart - 1; i < change.originalEnd; i++) {
383
- diffLines.push(`-${originalLines[i]}`);
384
- }
385
- // Show added lines
386
- for (let i = change.currentStart - 1; i < change.currentEnd; i++) {
387
- diffLines.push(`+${currentLines[i]}`);
388
- }
389
- break;
390
- case 'added':
391
- for (let i = change.currentStart - 1; i < change.currentEnd; i++) {
392
- diffLines.push(`+${currentLines[i]}`);
393
- }
394
- break;
395
- case 'removed':
396
- for (let i = change.originalStart - 1; i < change.originalEnd; i++) {
397
- diffLines.push(`-${originalLines[i]}`);
398
- }
399
- break;
400
- }
401
- // Add context after
402
- for (let i = change.originalEnd; i < Math.min(change.originalEnd + contextLines, originalLines.length); i++) {
403
- diffLines.push(` ${originalLines[i]}`);
404
- }
405
- // Add separator between hunks if there are more changes
406
- if (changes.indexOf(change) < changes.length - 1) {
407
- diffLines.push('');
408
- }
409
- }
410
- return diffLines.join('\n');
411
- }
412
- /**
413
- * Find meaningful changes between two sets of lines
414
- */
415
- findMeaningfulChanges(originalLines, currentLines) {
416
- const changes = [];
417
- let originalIndex = 0;
418
- let currentIndex = 0;
419
- while (originalIndex < originalLines.length || currentIndex < currentLines.length) {
420
- // Skip identical lines
421
- while (originalIndex < originalLines.length &&
422
- currentIndex < currentLines.length &&
423
- this.normalizeLineForComparison(originalLines[originalIndex]) ===
424
- this.normalizeLineForComparison(currentLines[currentIndex])) {
425
- originalIndex++;
426
- currentIndex++;
427
- }
428
- if (originalIndex >= originalLines.length && currentIndex >= currentLines.length) {
429
- break;
430
- }
431
- // Find the end of the different section
432
- const changeStartOriginal = originalIndex;
433
- const changeStartCurrent = currentIndex;
434
- // Look ahead to find where lines become similar again
435
- let foundMatch = false;
436
- let lookAhead = 5; // Look ahead up to 5 lines to find a match
437
- for (let ahead = 1; ahead <= lookAhead && !foundMatch; ahead++) {
438
- for (let origOffset = 0; origOffset <= ahead && !foundMatch; origOffset++) {
439
- const currOffset = ahead - origOffset;
440
- if (originalIndex + origOffset < originalLines.length &&
441
- currentIndex + currOffset < currentLines.length) {
442
- if (this.normalizeLineForComparison(originalLines[originalIndex + origOffset]) ===
443
- this.normalizeLineForComparison(currentLines[currentIndex + currOffset])) {
444
- // Found a match, determine change type
445
- if (origOffset === 0 && currOffset > 0) {
446
- // Lines were added
447
- changes.push({
448
- type: 'added',
449
- originalStart: originalIndex + 1,
450
- originalEnd: originalIndex + 1,
451
- currentStart: currentIndex + 1,
452
- currentEnd: currentIndex + currOffset,
453
- description: `Added ${currOffset} lines`
454
- });
455
- currentIndex += currOffset;
456
- }
457
- else if (origOffset > 0 && currOffset === 0) {
458
- // Lines were removed
459
- changes.push({
460
- type: 'removed',
461
- originalStart: originalIndex + 1,
462
- originalEnd: originalIndex + origOffset,
463
- currentStart: currentIndex + 1,
464
- currentEnd: currentIndex + 1,
465
- description: `Removed ${origOffset} lines`
466
- });
467
- originalIndex += origOffset;
468
- }
469
- else if (origOffset > 0 && currOffset > 0) {
470
- // Lines were modified
471
- changes.push({
472
- type: 'modified',
473
- originalStart: originalIndex + 1,
474
- originalEnd: originalIndex + origOffset,
475
- currentStart: currentIndex + 1,
476
- currentEnd: currentIndex + currOffset,
477
- description: `Modified ${Math.max(origOffset, currOffset)} lines`
478
- });
479
- originalIndex += origOffset;
480
- currentIndex += currOffset;
481
- }
482
- foundMatch = true;
483
- }
484
- }
485
- }
486
- }
487
- if (!foundMatch) {
488
- // No match found, treat remaining as changes
489
- if (originalIndex < originalLines.length && currentIndex < currentLines.length) {
490
- // Both have remaining lines - treat as modification
491
- const remainingOriginal = originalLines.length - originalIndex;
492
- const remainingCurrent = currentLines.length - currentIndex;
493
- changes.push({
494
- type: 'modified',
495
- originalStart: originalIndex + 1,
496
- originalEnd: originalLines.length,
497
- currentStart: currentIndex + 1,
498
- currentEnd: currentLines.length,
499
- description: `Modified remaining lines (${remainingOriginal} → ${remainingCurrent})`
500
- });
501
- break;
502
- }
503
- else if (originalIndex < originalLines.length) {
504
- // Only original has remaining lines - removed
505
- changes.push({
506
- type: 'removed',
507
- originalStart: originalIndex + 1,
508
- originalEnd: originalLines.length,
509
- currentStart: currentIndex + 1,
510
- currentEnd: currentIndex + 1,
511
- description: `Removed ${originalLines.length - originalIndex} lines`
512
- });
513
- break;
514
- }
515
- else if (currentIndex < currentLines.length) {
516
- // Only current has remaining lines - added
517
- changes.push({
518
- type: 'added',
519
- originalStart: originalIndex + 1,
520
- originalEnd: originalIndex + 1,
521
- currentStart: currentIndex + 1,
522
- currentEnd: currentLines.length,
523
- description: `Added ${currentLines.length - currentIndex} lines`
524
- });
525
- break;
526
- }
527
- }
528
- }
529
- return changes;
530
- }
531
- /**
532
- * Normalize line for comparison (remove extra whitespace, etc.)
533
- */
534
- normalizeLineForComparison(line) {
535
- return line.trim().replace(/\s+/g, ' ');
536
- }
537
- /**
538
- * Get commit history
539
- */
540
- async getCommitHistory(params) {
541
- try {
542
- const gitApi = await this.getGitApi();
543
- // Create comprehensive search criteria
544
- const searchCriteria = {
545
- itemPath: params.itemPath,
546
- $skip: params.skip || 0,
547
- $top: params.top || 100, // Default to 100 if not specified
548
- includeStatuses: true,
549
- includeWorkItems: true
550
- };
551
- // Get commits with proper search criteria for richer data
552
- // Resolve repository name/ID to actual repository ID
553
- const project = params.projectId || this.config.project;
554
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
555
- const commits = await gitApi.getCommits(repositoryId, searchCriteria, project);
556
- // The commits are already filtered and paginated by the API
557
- return commits || [];
558
- }
559
- catch (error) {
560
- console.error(`Error getting commit history for repository ${params.repository}:`, error);
561
- throw error;
562
- }
563
- }
564
- /**
565
- * Get commits
566
- */
567
- async getCommits(params) {
568
- try {
569
- const gitApi = await this.getGitApi();
570
- // Resolve repository name/ID to actual repository ID
571
- const repositoryId = await this.resolveRepositoryId(params.repository);
572
- // Get commits without search criteria
573
- const commits = await gitApi.getCommits(repositoryId, {} // Empty search criteria
574
- );
575
- // Filter by path if provided
576
- let filteredCommits = commits;
577
- if (params.path) {
578
- filteredCommits = commits.filter(commit => commit.comment && commit.comment.includes(params.path || ""));
579
- }
580
- return filteredCommits;
581
- }
582
- catch (error) {
583
- console.error(`Error getting commits for repository ${params.repository}:`, error);
584
- throw error;
585
- }
586
- }
587
- /**
588
- * Get pull requests
589
- */
590
- async getPullRequests(params) {
591
- try {
592
- const gitApi = await this.getGitApi();
593
- const project = params.project || params.projectId || this.config.project;
594
- // Resolve repository name/ID to actual repository ID
595
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
596
- // Create search criteria with proper types
597
- const searchCriteria = {
598
- repositoryId: repositoryId,
599
- creatorId: params.creatorId,
600
- reviewerId: params.reviewerId,
601
- sourceRefName: params.sourceRefName,
602
- targetRefName: params.targetRefName
603
- };
604
- // Convert string status to number if provided
605
- if (params.status) {
606
- if (params.status === 'active')
607
- searchCriteria.status = 1;
608
- else if (params.status === 'abandoned')
609
- searchCriteria.status = 2;
610
- else if (params.status === 'completed')
611
- searchCriteria.status = 3;
612
- else if (params.status === 'notSet')
613
- searchCriteria.status = 0;
614
- // 'all' doesn't need to be set
615
- }
616
- const pullRequests = await this.withAuthRetry(() => gitApi.getPullRequests(repositoryId, searchCriteria, project, undefined, // maxCommentLength
617
- params.skip || 0, params.top || 50), {
618
- operationName: 'git.pullRequests.list',
619
- details: {
620
- project,
621
- repositoryId,
622
- top: params.top || 50,
623
- skip: params.skip || 0,
624
- status: params.status || 'default',
625
- },
626
- });
627
- // Note: Work item integration could be added here in the future
628
- // For now, we return the basic pull request data with rich reviewer information
629
- return pullRequests;
630
- }
631
- catch (error) {
632
- console.error(`Error getting pull requests for repository ${params.repository}:`, error);
633
- throw error;
634
- }
635
- }
636
- /**
637
- * Create pull request
638
- */
639
- async createPullRequest(params) {
640
- try {
641
- const gitApi = await this.getGitApi();
642
- const project = params.project || this.config.project;
643
- const pullRequest = {
644
- sourceRefName: params.sourceRefName,
645
- targetRefName: params.targetRefName,
646
- title: params.title,
647
- description: params.description,
648
- reviewers: params.reviewers ? params.reviewers.map(id => ({ id })) : undefined
649
- };
650
- // Resolve repository name/ID to actual repository ID
651
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
652
- const createdPullRequest = await gitApi.createPullRequest(pullRequest, repositoryId, project);
653
- return createdPullRequest;
654
- }
655
- catch (error) {
656
- console.error('Error creating pull request:', error);
657
- throw error;
658
- }
659
- }
660
- /**
661
- * Get pull request by ID
662
- */
663
- /**
664
- * Get policy evaluations for a pull request
665
- */
666
- async getPolicyEvaluations(repository, pullRequestId, projectOverride) {
667
- try {
668
- const policyApi = await this.connection.getPolicyApi();
669
- const effectiveProject = projectOverride || this.config.project;
670
- // Need project GUID for artifact ID
671
- const coreApi = await this.connection.getCoreApi();
672
- const project = await coreApi.getProject(effectiveProject);
673
- const projectId = project.id;
674
- // Artifact ID format uses %2F between segments (per MEMORY.md)
675
- const artifactId = `vstfs:///CodeReview/CodeReviewId/${projectId}%2F${pullRequestId}`;
676
- const evaluations = await policyApi.getPolicyEvaluations(effectiveProject, artifactId);
677
- return evaluations || [];
678
- }
679
- catch (error) {
680
- console.error(`Error fetching policy evaluations for PR ${pullRequestId}:`, error);
681
- return [];
682
- }
683
- }
684
- async getPullRequest(params) {
685
- try {
686
- const gitApi = await this.getGitApi();
687
- const project = params.project || this.config.project;
688
- // Resolve repository name/ID to actual repository ID
689
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
690
- const pullRequest = await this.withAuthRetry(() => gitApi.getPullRequest(repositoryId, params.pullRequestId, project), {
691
- operationName: 'git.pullRequests.get',
692
- details: {
693
- project,
694
- repositoryId,
695
- pullRequestId: params.pullRequestId,
696
- },
697
- });
698
- // Create enhanced response with work items
699
- const enhancedPullRequest = { ...pullRequest };
700
- // Fetch associated work items
701
- try {
702
- const workItemRefs = await gitApi.getPullRequestWorkItemRefs(repositoryId, params.pullRequestId, project);
703
- if (workItemRefs && workItemRefs.length > 0) {
704
- // Get work item IDs from references
705
- const workItemIds = workItemRefs
706
- .map(ref => {
707
- // Extract work item ID from URL (format: .../workitems/123 or .../workItems/123)
708
- const match = ref.url?.match(/workitems?\/(\d+)/i);
709
- return match ? parseInt(match[1]) : null;
710
- })
711
- .filter((id) => id !== null);
712
- if (workItemIds.length > 0) {
713
- // Fetch work item details
714
- const witApi = await this.connection.getWorkItemTrackingApi();
715
- const workItems = await witApi.getWorkItems(workItemIds, undefined, undefined, undefined, undefined, project);
716
- // Add work items to pull request object
717
- enhancedPullRequest.workItems = workItems.map(wi => ({
718
- id: wi.id,
719
- title: wi.fields?.['System.Title'],
720
- state: wi.fields?.['System.State'],
721
- type: wi.fields?.['System.WorkItemType'],
722
- assignedTo: wi.fields?.['System.AssignedTo']?.displayName,
723
- url: wi.url
724
- }));
725
- }
726
- }
727
- }
728
- catch (workItemError) {
729
- // Log but don't fail the PR fetch if work items can't be retrieved
730
- console.error(`Error fetching work items for PR ${params.pullRequestId}:`, workItemError);
731
- enhancedPullRequest.workItems = [];
732
- }
733
- // Fetch policy evaluations (always in compact mode, or when explicitly requested)
734
- const include = params.include;
735
- if (!include || include.length === 0 || include.includes('policies')) {
736
- try {
737
- const evaluations = await this.getPolicyEvaluations(params.repository, params.pullRequestId, project);
738
- enhancedPullRequest.policyEvaluations = evaluations;
739
- }
740
- catch {
741
- enhancedPullRequest.policyEvaluations = [];
742
- }
743
- }
744
- // Pass include through so the formatter knows what mode to use
745
- enhancedPullRequest._include = include;
746
- return enhancedPullRequest;
747
- }
748
- catch (error) {
749
- console.error(`Error getting pull request ${params.pullRequestId}:`, error);
750
- throw error;
751
- }
752
- }
753
- /**
754
- * Get pull request comments
755
- */
756
- async getPullRequestComments(params) {
757
- try {
758
- const gitApi = await this.getGitApi();
759
- const project = params.project || this.config.project;
760
- // Resolve repository name/ID to actual repository ID
761
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
762
- if (params.threadId) {
763
- const thread = await gitApi.getPullRequestThread(repositoryId, params.pullRequestId, params.threadId, project);
764
- return thread;
765
- }
766
- else {
767
- let threads = await gitApi.getThreads(repositoryId, params.pullRequestId, project);
768
- // Apply status filter
769
- if (params.status && threads) {
770
- const statusMap = {
771
- 'active': 1, 'fixed': 2, 'wontFix': 3, 'closed': 4, 'byDesign': 5, 'pending': 6,
772
- };
773
- const targetStatus = statusMap[params.status];
774
- if (targetStatus !== undefined) {
775
- threads = threads.filter((t) => t.status === targetStatus);
776
- }
777
- }
778
- // Apply author filter (case-insensitive match on display name or unique name)
779
- if (params.authorName && threads) {
780
- const authorLower = params.authorName.toLowerCase();
781
- threads = threads.filter((t) => t.comments?.some((c) => c.parentCommentId === 0 && (c.author?.displayName?.toLowerCase().includes(authorLower) ||
782
- c.author?.uniqueName?.toLowerCase().includes(authorLower))));
783
- }
784
- return threads;
785
- }
786
- }
787
- catch (error) {
788
- console.error(`Error getting comments for pull request ${params.pullRequestId}:`, error);
789
- throw error;
790
- }
791
- }
792
- /**
793
- * Approve pull request
794
- */
795
- async approvePullRequest(params) {
796
- try {
797
- const gitApi = await this.getGitApi();
798
- const project = params.project || this.config.project;
799
- // Resolve repository name/ID to actual repository ID
800
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
801
- // Resolve current user's identity (the "me" shorthand isn't supported by all ADO configurations)
802
- const reviewerId = await this.getAuthenticatedUserId();
803
- const vote = {
804
- vote: 10
805
- };
806
- const result = await gitApi.createPullRequestReviewer(vote, repositoryId, params.pullRequestId, reviewerId, project);
807
- return result;
808
- }
809
- catch (error) {
810
- console.error(`Error approving pull request ${params.pullRequestId}:`, error);
811
- throw error;
812
- }
813
- }
814
- /**
815
- * Merge pull request
816
- */
817
- async mergePullRequest(params) {
818
- try {
819
- const gitApi = await this.getGitApi();
820
- const project = params.project || this.config.project;
821
- // Convert string merge strategy to number
822
- let mergeStrategy = 1; // Default to noFastForward
823
- if (params.mergeStrategy === 'rebase')
824
- mergeStrategy = 2;
825
- else if (params.mergeStrategy === 'rebaseMerge')
826
- mergeStrategy = 3;
827
- else if (params.mergeStrategy === 'squash')
828
- mergeStrategy = 4;
829
- let repositoryId = await this.resolveRepositoryId(params.repository, project);
830
- const result = await gitApi.updatePullRequest({
831
- status: 3, // 3 = completed in PullRequestStatus enum
832
- completionOptions: {
833
- mergeStrategy: mergeStrategy
834
- }
835
- }, repositoryId, params.pullRequestId, project);
836
- return result;
837
- }
838
- catch (error) {
839
- console.error(`Error merging pull request ${params.pullRequestId}:`, error);
840
- throw error;
841
- }
842
- }
843
- /**
844
- * Complete pull request
845
- */
846
- async completePullRequest(params) {
847
- try {
848
- const gitApi = await this.getGitApi();
849
- const project = params.project || params.projectId || this.config.project;
850
- // Get the current pull request
851
- const pullRequest = await gitApi.getPullRequestById(params.pullRequestId);
852
- // Convert string merge strategy to number
853
- let mergeStrategy = 1; // Default to noFastForward
854
- if (params.mergeStrategy === 'rebase')
855
- mergeStrategy = 2;
856
- else if (params.mergeStrategy === 'rebaseMerge')
857
- mergeStrategy = 3;
858
- else if (params.mergeStrategy === 'squash')
859
- mergeStrategy = 4;
860
- let repositoryId = await this.resolveRepositoryId(params.repository, project);
861
- // Update the pull request to completed status
862
- const updatedPullRequest = await gitApi.updatePullRequest({
863
- status: 3, // 3 = completed in PullRequestStatus enum
864
- completionOptions: {
865
- mergeStrategy: mergeStrategy,
866
- deleteSourceBranch: params.deleteSourceBranch
867
- }
868
- }, repositoryId, params.pullRequestId);
869
- return updatedPullRequest;
870
- }
871
- catch (error) {
872
- console.error(`Error completing pull request ${params.pullRequestId}:`, error);
873
- throw error;
874
- }
875
- }
876
- /**
877
- * Add inline comment to pull request
878
- */
879
- async addPullRequestInlineComment(params) {
880
- try {
881
- const gitApi = await this.getGitApi();
882
- const project = params.project || this.config.project;
883
- let repositoryId = await this.resolveRepositoryId(params.repository, project);
884
- // Get the latest iteration number
885
- const latestIteration = await this.getLatestPullRequestIteration(repositoryId, params.pullRequestId, project);
886
- // Get the changes for the file to get the change tracking ID
887
- const changes = await gitApi.getPullRequestIterationChanges(repositoryId, params.pullRequestId, latestIteration, project);
888
- // Helper function to normalize paths by removing leading slash
889
- const normalizePath = (path) => {
890
- return path.startsWith('/') ? path.substring(1) : path;
891
- };
892
- // Normalize the request path for consistent matching
893
- const normalizedRequestPath = normalizePath(params.path);
894
- // Find the change entry for the specific file using normalized path matching
895
- const changeEntry = changes.changeEntries?.find(entry => {
896
- if (!entry.item?.path)
897
- return false;
898
- const normalizedEntryPath = normalizePath(entry.item.path);
899
- return normalizedEntryPath === normalizedRequestPath;
900
- });
901
- if (!changeEntry) {
902
- // Provide a more helpful error message with available files
903
- const availableFiles = changes.changeEntries?.map(entry => entry.item?.path).filter((path) => Boolean(path)) || [];
904
- const fileList = availableFiles.length > 0
905
- ? `\n\nFiles changed in this PR:\n${availableFiles.map(file => `- ${file}`).join('\n')}`
906
- : '\n\nNo files found in this PR.';
907
- throw new Error(`File '${params.path}' is not part of the changes in this pull request.${fileList}
908
-
909
- 💡 **To find the correct files and line numbers:**
910
-
911
- Use 'getPullRequestFileChanges' with:
912
- - repository: '${params.repository}'
913
- - pullRequestId: ${params.pullRequestId}
914
-
915
- This will show you the actual file changes and line numbers available for commenting.
916
-
917
- Note: You can only add inline comments to files that have been modified in the PR.`);
918
- }
919
- // Determine thread context based on change type
920
- let threadContext = {
921
- filePath: params.path,
922
- };
923
- // Compute end position: use explicit endLine/endOffset if provided, otherwise default to same line +1 offset
924
- const endLine = params.position.endLine ?? params.position.line;
925
- const endOffset = params.position.endOffset ?? (params.position.offset + 1);
926
- // Handle different change types according to Azure DevOps API documentation
927
- if (changeEntry.changeType === GitInterfaces_1.VersionControlChangeType.Add) {
928
- // ADDED file: leftFile positions should be null, rightFile positions are for the new file
929
- threadContext.leftFileStart = null;
930
- threadContext.leftFileEnd = null;
931
- threadContext.rightFileStart = {
932
- line: params.position.line,
933
- offset: params.position.offset
934
- };
935
- threadContext.rightFileEnd = {
936
- line: endLine,
937
- offset: endOffset
938
- };
939
- }
940
- else if (changeEntry.changeType === GitInterfaces_1.VersionControlChangeType.Delete) {
941
- // DELETED file: rightFile positions should be null, leftFile positions are for the deleted content
942
- threadContext.leftFileStart = {
943
- line: params.position.line,
944
- offset: params.position.offset
945
- };
946
- threadContext.leftFileEnd = {
947
- line: endLine,
948
- offset: endOffset
949
- };
950
- threadContext.rightFileStart = null;
951
- threadContext.rightFileEnd = null;
952
- }
953
- else {
954
- // MODIFIED file: both left and right positions can be set (traditional approach)
955
- threadContext.leftFileStart = null; // Often null for simple line comments
956
- threadContext.leftFileEnd = null;
957
- threadContext.rightFileStart = {
958
- line: params.position.line,
959
- offset: params.position.offset
960
- };
961
- threadContext.rightFileEnd = {
962
- line: endLine,
963
- offset: endOffset
964
- };
965
- }
966
- // Create a thread with proper context for the comment
967
- // PR comments are markdown-native (client-side rendered, no server-side format param).
968
- // Normalise literal \n escapes that LLMs sometimes double-escape in tool-call JSON.
969
- const content = params.format === 'html'
970
- ? params.comment
971
- : (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.comment));
972
- const thread = {
973
- comments: [{
974
- content,
975
- parentCommentId: 0,
976
- commentType: 1 // 1 = text
977
- }],
978
- status: 1, // 1 = active
979
- threadContext,
980
- pullRequestThreadContext: {
981
- changeTrackingId: changeEntry.changeTrackingId, // Use the change tracking ID from the diff
982
- iterationContext: {
983
- firstComparingIteration: 1, // First iteration
984
- secondComparingIteration: 1 // Current iteration
985
- }
986
- }
987
- };
988
- // Create the thread (which includes the comment)
989
- const result = await gitApi.createThread(thread, repositoryId, params.pullRequestId, project);
990
- return result;
991
- }
992
- catch (error) {
993
- console.error(`Error adding inline comment to pull request ${params.pullRequestId}:`, error);
994
- // Enhanced error handling for common scenarios
995
- if (error.message || error.response?.data?.message) {
996
- const errorMessage = error.message || error.response?.data?.message || '';
997
- // Check for line number related errors
998
- if (errorMessage.includes('line') ||
999
- errorMessage.includes('position') ||
1000
- errorMessage.includes('range') ||
1001
- errorMessage.includes('invalid') ||
1002
- error.status === 400 ||
1003
- error.statusCode === 400) {
1004
- throw new Error(`Unable to add inline comment at line ${params.position.line} in file '${params.path}'. This could be because:
1005
-
1006
- • The line number doesn't exist in the file
1007
- • The line position is outside the valid range
1008
- • For newly added files: line numbers start from 1 and go up to the total lines in the new file
1009
- • For modified files: only changed line ranges can be commented on
1010
- • For deleted files: only the original line numbers from the deleted content can be commented on
1011
-
1012
- 💡 **Solution:** Use 'getPullRequestFileChanges' with repository: '${params.repository}' and pullRequestId: ${params.pullRequestId} to:
1013
- - See the actual code diff for '${params.path}'
1014
- - For NEW files (Add): All lines (1 to N) are available for comments, shown as +1, +2, +3...
1015
- - For MODIFIED files (Edit): Only changed line ranges can be commented on
1016
- - For DELETED files (Delete): Only deleted line numbers can be commented on, shown as -1, -2, -3...
1017
- - View the exact changes and valid line numbers
1018
-
1019
- **Based on Azure DevOps REST API research:** The thread context is now properly configured for each change type.
1020
-
1021
- Original error: ${errorMessage}`);
1022
- }
1023
- }
1024
- throw error;
1025
- }
1026
- }
1027
- /**
1028
- * Add file comment to pull request
1029
- */
1030
- async addPullRequestFileComment(params) {
1031
- try {
1032
- const gitApi = await this.getGitApi();
1033
- const project = params.project || this.config.project;
1034
- // Create a thread with proper context for a file-level comment
1035
- // PR comments are markdown-native — normalise literal \n escapes from LLM tool calls.
1036
- const content = params.format === 'html'
1037
- ? params.comment
1038
- : (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.comment));
1039
- const thread = {
1040
- comments: [{
1041
- content,
1042
- parentCommentId: 0,
1043
- commentType: 1 // 1 = text
1044
- }],
1045
- status: 1, // 1 = active
1046
- threadContext: {
1047
- filePath: params.path
1048
- // No position info for file-level comments
1049
- }
1050
- };
1051
- // Resolve repository name/ID to actual repository ID
1052
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1053
- // Create the thread (which includes the comment)
1054
- const result = await gitApi.createThread(thread, repositoryId, params.pullRequestId, project);
1055
- return result;
1056
- }
1057
- catch (error) {
1058
- console.error(`Error adding file comment to pull request ${params.pullRequestId}:`, error);
1059
- throw error;
1060
- }
1061
- }
1062
- /**
1063
- * Add general comment to pull request
1064
- */
1065
- async addPullRequestComment(params) {
1066
- try {
1067
- const gitApi = await this.getGitApi();
1068
- const project = params.project || this.config.project;
1069
- // Create a thread for a general PR comment (no file context)
1070
- // PR comments are markdown-native — normalise literal \n escapes from LLM tool calls.
1071
- const content = params.format === 'html'
1072
- ? params.comment
1073
- : (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.comment));
1074
- const thread = {
1075
- comments: [{
1076
- content,
1077
- parentCommentId: 0,
1078
- commentType: 1 // 1 = text
1079
- }],
1080
- status: 1 // 1 = active
1081
- // No threadContext for general PR comments
1082
- };
1083
- // Resolve repository name/ID to actual repository ID
1084
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1085
- // Create the thread (which includes the comment)
1086
- const result = await gitApi.createThread(thread, repositoryId, params.pullRequestId, project);
1087
- return result;
1088
- }
1089
- catch (error) {
1090
- console.error(`Error adding comment to pull request ${params.pullRequestId}:`, error);
1091
- throw error;
1092
- }
1093
- }
1094
- /**
1095
- * Get pull request file changes with diff content
1096
- */
1097
- async getPullRequestFileChanges(params) {
1098
- try {
1099
- const gitApi = await this.getGitApi();
1100
- const project = params.project || this.config.project;
1101
- // Resolve repository name/ID to actual repository ID
1102
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1103
- // If path is provided, we need to get the changes for that specific file
1104
- if (params.path) {
1105
- // Get the latest iteration number
1106
- const iterationNumber = await this.getLatestPullRequestIteration(repositoryId, params.pullRequestId, project);
1107
- let changes = null;
1108
- try {
1109
- // Get the changes from the latest iteration
1110
- changes = await gitApi.getPullRequestIterationChanges(repositoryId, params.pullRequestId, iterationNumber, project);
1111
- }
1112
- catch (error) {
1113
- console.error('Error getting PR changes:', error);
1114
- throw error;
1115
- }
1116
- // Helper function to normalize paths by removing leading slash
1117
- const normalizePath = (path) => {
1118
- return path.startsWith('/') ? path.substring(1) : path;
1119
- };
1120
- // Normalize the request path for consistent matching
1121
- const normalizedRequestPath = normalizePath(params.path);
1122
- // Filter changes for the specific file with improved path matching
1123
- const filteredChanges = {
1124
- ...changes,
1125
- changeEntries: changes.changeEntries?.filter((entry) => {
1126
- if (!entry.item?.path)
1127
- return false;
1128
- const entryPath = entry.item.path;
1129
- const normalizedEntryPath = normalizePath(entryPath);
1130
- // Compare normalized paths for consistent matching regardless of leading slash
1131
- return normalizedEntryPath === normalizedRequestPath;
1132
- }) || []
1133
- };
1134
- // Enhance with diff content for each change
1135
- const enhancedChangeEntries = await Promise.all(filteredChanges.changeEntries.map(async (change) => {
1136
- let diffContent = '';
1137
- if (change.changeType === 2 && change.item?.originalObjectId && change.item?.objectId) {
1138
- // Modified file - get both versions and calculate diff
1139
- try {
1140
- const [originalContent, currentContent] = await Promise.all([
1141
- this.getFileContentByObjectId(repositoryId, change.item.originalObjectId, project),
1142
- this.getFileContentByObjectId(repositoryId, change.item.objectId, project)
1143
- ]);
1144
- diffContent = this.calculateUnifiedDiff(originalContent, currentContent, change.item.path);
1145
- diffContent = this.addInlineCommentGuidance(diffContent, change.changeType);
1146
- }
1147
- catch (error) {
1148
- console.error(`Error getting diff for ${change.item.path}:`, error);
1149
- diffContent = '[Diff not available]';
1150
- }
1151
- }
1152
- else if (change.changeType === 1 && change.item?.objectId) {
1153
- // Added file - get new content and format as all additions
1154
- try {
1155
- const newContent = await this.getFileContentByObjectId(repositoryId, change.item.objectId, project);
1156
- diffContent = this.calculateAddedFileDiff(newContent, change.item.path);
1157
- }
1158
- catch (error) {
1159
- console.error(`Error getting content for new file ${change.item.path}:`, error);
1160
- diffContent = '[Content not available]';
1161
- }
1162
- }
1163
- else if (change.changeType === 3 && change.item?.originalObjectId) {
1164
- // Deleted file - get original content and format as all deletions
1165
- try {
1166
- const originalContent = await this.getFileContentByObjectId(repositoryId, change.item.originalObjectId, project);
1167
- diffContent = this.calculateDeletedFileDiff(originalContent, change.item.path);
1168
- }
1169
- catch (error) {
1170
- console.error(`Error getting content for deleted file ${change.item.path}:`, error);
1171
- diffContent = '[Content not available]';
1172
- }
1173
- }
1174
- return {
1175
- ...change,
1176
- diffContent
1177
- };
1178
- }));
1179
- return {
1180
- ...filteredChanges,
1181
- changeEntries: enhancedChangeEntries
1182
- };
1183
- }
1184
- // If no path is provided, get all changes with diffs
1185
- // Get the latest iteration number
1186
- const latestIteration = await this.getLatestPullRequestIteration(repositoryId, params.pullRequestId, project);
1187
- const changes = await gitApi.getPullRequestIterationChanges(repositoryId, params.pullRequestId, latestIteration, project);
1188
- // Enhance with diff content for each change (smart selection to show variety)
1189
- const allChanges = changes.changeEntries || [];
1190
- // Smart selection: try to get examples of different change types
1191
- const modifiedFiles = allChanges.filter((c) => c.changeType === 2); // Modified
1192
- const addedFiles = allChanges.filter((c) => c.changeType === 1); // Added
1193
- const deletedFiles = allChanges.filter((c) => c.changeType === 3); // Deleted
1194
- let changesToProcess = [];
1195
- // Take up to 2 from each type, prioritizing modified files, then added, then deleted
1196
- changesToProcess.push(...modifiedFiles.slice(0, 2));
1197
- changesToProcess.push(...addedFiles.slice(0, 2));
1198
- changesToProcess.push(...deletedFiles.slice(0, 1));
1199
- // If we have fewer than 5, fill up with remaining files
1200
- if (changesToProcess.length < 5) {
1201
- const remainingFiles = allChanges.filter((c) => !changesToProcess.includes(c));
1202
- changesToProcess.push(...remainingFiles.slice(0, 5 - changesToProcess.length));
1203
- }
1204
- // Limit to 5 files total for performance
1205
- changesToProcess = changesToProcess.slice(0, 5);
1206
- const enhancedChangeEntries = await Promise.all(changesToProcess.map(async (change) => {
1207
- let diffContent = '';
1208
- if (change.changeType === 2 && change.item?.originalObjectId && change.item?.objectId) {
1209
- // Modified file - get both versions and calculate diff
1210
- try {
1211
- const [originalContent, currentContent] = await Promise.all([
1212
- this.getFileContentByObjectId(repositoryId, change.item.originalObjectId, project),
1213
- this.getFileContentByObjectId(repositoryId, change.item.objectId, project)
1214
- ]);
1215
- diffContent = this.calculateUnifiedDiff(originalContent, currentContent, change.item.path);
1216
- diffContent = this.addInlineCommentGuidance(diffContent, change.changeType);
1217
- }
1218
- catch (error) {
1219
- console.error(`Error getting diff for ${change.item.path}:`, error);
1220
- diffContent = '[Diff not available]';
1221
- }
1222
- }
1223
- else if (change.changeType === 1 && change.item?.objectId) {
1224
- // Added file - get new content and format as all additions
1225
- try {
1226
- const newContent = await this.getFileContentByObjectId(repositoryId, change.item.objectId, project);
1227
- diffContent = this.calculateAddedFileDiff(newContent, change.item.path);
1228
- }
1229
- catch (error) {
1230
- console.error(`Error getting content for new file ${change.item.path}:`, error);
1231
- diffContent = '[Content not available]';
1232
- }
1233
- }
1234
- else if (change.changeType === 3 && change.item?.originalObjectId) {
1235
- // Deleted file - get original content and format as all deletions
1236
- try {
1237
- const originalContent = await this.getFileContentByObjectId(repositoryId, change.item.originalObjectId, project);
1238
- diffContent = this.calculateDeletedFileDiff(originalContent, change.item.path);
1239
- }
1240
- catch (error) {
1241
- console.error(`Error getting content for deleted file ${change.item.path}:`, error);
1242
- diffContent = '[Content not available]';
1243
- }
1244
- }
1245
- return {
1246
- ...change,
1247
- diffContent
1248
- };
1249
- }));
1250
- return {
1251
- ...changes,
1252
- changeEntries: enhancedChangeEntries,
1253
- totalChanges: changes.changeEntries?.length || 0,
1254
- processedChanges: enhancedChangeEntries.length
1255
- };
1256
- }
1257
- catch (error) {
1258
- console.error(`Error getting file changes for pull request ${params.pullRequestId}:`, error);
1259
- throw error;
1260
- }
1261
- }
1262
- /**
1263
- * Get pull request changes count
1264
- */
1265
- async getPullRequestChangesCount(params) {
1266
- try {
1267
- const gitApi = await this.getGitApi();
1268
- const project = params.project || this.config.project;
1269
- // Resolve repository name/ID to actual repository ID
1270
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1271
- // Get the latest iteration number
1272
- const latestIteration = await this.getLatestPullRequestIteration(repositoryId, params.pullRequestId, project);
1273
- const changes = await gitApi.getPullRequestIterationChanges(repositoryId, params.pullRequestId, latestIteration, project);
1274
- return {
1275
- totalChanges: changes.changeEntries?.length || 0,
1276
- addedFiles: changes.changeEntries?.filter(entry => entry.changeType === GitInterfaces_1.VersionControlChangeType.Add).length || 0,
1277
- modifiedFiles: changes.changeEntries?.filter(entry => entry.changeType === GitInterfaces_1.VersionControlChangeType.Edit).length || 0,
1278
- deletedFiles: changes.changeEntries?.filter(entry => entry.changeType === GitInterfaces_1.VersionControlChangeType.Delete).length || 0
1279
- };
1280
- }
1281
- catch (error) {
1282
- console.error(`Error getting changes count for pull request ${params.pullRequestId}:`, error);
1283
- throw error;
1284
- }
1285
- }
1286
- /**
1287
- * Get all pull request changes
1288
- */
1289
- async getAllPullRequestChanges(params) {
1290
- try {
1291
- const gitApi = await this.getGitApi();
1292
- const project = params.project || this.config.project;
1293
- // Resolve repository name/ID to actual repository ID
1294
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1295
- // Get the latest iteration number
1296
- const latestIteration = await this.getLatestPullRequestIteration(repositoryId, params.pullRequestId, project);
1297
- const changes = await gitApi.getPullRequestIterationChanges(repositoryId, params.pullRequestId, latestIteration, project);
1298
- let changeEntries = changes.changeEntries || [];
1299
- // Apply pagination if specified
1300
- if (params.skip && params.skip > 0) {
1301
- changeEntries = changeEntries.slice(params.skip);
1302
- }
1303
- if (params.top && params.top > 0) {
1304
- changeEntries = changeEntries.slice(0, params.top);
1305
- }
1306
- return {
1307
- changes: changeEntries,
1308
- totalCount: changes.changeEntries?.length || 0
1309
- };
1310
- }
1311
- catch (error) {
1312
- console.error(`Error getting all changes for pull request ${params.pullRequestId}:`, error);
1313
- throw error;
1314
- }
1315
- }
1316
- /**
1317
- * Get list of changed files in a pull request (useful for knowing which files can have inline comments)
1318
- */
1319
- async getPullRequestChangedFilesList(repositoryId, pullRequestId, project) {
1320
- try {
1321
- const gitApi = await this.getGitApi();
1322
- const effectiveProject = project || this.config.project;
1323
- // Get the latest iteration number
1324
- const latestIteration = await this.getLatestPullRequestIteration(repositoryId, pullRequestId, effectiveProject);
1325
- const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, latestIteration, effectiveProject);
1326
- return changes.changeEntries?.map(entry => entry.item?.path).filter((path) => Boolean(path)) || [];
1327
- }
1328
- catch (error) {
1329
- console.error(`Error getting changed files list for PR ${pullRequestId}:`, error);
1330
- return [];
1331
- }
1332
- }
1333
- /**
1334
- * Calculate diff for an added file (all content is new)
1335
- */
1336
- calculateAddedFileDiff(content, filePath) {
1337
- const lines = content.split('\n');
1338
- if (lines.length === 0) {
1339
- return `--- /dev/null
1340
- +++ b${filePath}
1341
- @@ -0,0 +1,0 @@
1342
- 📄 NEW FILE (Empty)
1343
-
1344
- 💬 INLINE COMMENT LINES: None (empty file)`;
1345
- }
1346
- // For large files, show a summary instead of all content
1347
- const maxLinesToShow = 250;
1348
- const isLargeFile = lines.length > maxLinesToShow;
1349
- let diffLines = [];
1350
- diffLines.push(`--- /dev/null`);
1351
- diffLines.push(`+++ b${filePath}`);
1352
- diffLines.push(`@@ -0,0 +1,${lines.length} @@`);
1353
- diffLines.push(`📄 NEW FILE (${lines.length} lines)`);
1354
- diffLines.push(`💬 INLINE COMMENT LINES: 1 to ${lines.length} (any line can be commented)`);
1355
- diffLines.push(``);
1356
- if (isLargeFile) {
1357
- // Show first 50 lines with clear line numbers
1358
- const firstLines = lines.slice(0, 50);
1359
- const lastLines = lines.slice(-10);
1360
- const hiddenLineCount = lines.length - firstLines.length - lastLines.length;
1361
- // Add first 50 lines with line numbers
1362
- firstLines.forEach((line, index) => {
1363
- const lineNumber = index + 1;
1364
- diffLines.push(`+${lineNumber.toString().padStart(4, ' ')}: ${line} [← ${lineNumber}, right]`);
1365
- });
1366
- // Add summary of hidden content
1367
- if (hiddenLineCount > 0) {
1368
- const startHidden = firstLines.length + 1;
1369
- const endHidden = lines.length - lastLines.length;
1370
- diffLines.push(`+... (${hiddenLineCount} more lines: ${startHidden}-${endHidden}, all commentable) ...`);
1371
- }
1372
- // Add last 10 lines with line numbers
1373
- lastLines.forEach((line, index) => {
1374
- const lineNumber = lines.length - lastLines.length + index + 1;
1375
- diffLines.push(`+${lineNumber.toString().padStart(4, ' ')}: ${line} [← ${lineNumber}, right]`);
1376
- });
1377
- }
1378
- else {
1379
- // Show all content for smaller files with line numbers
1380
- lines.forEach((line, index) => {
1381
- const lineNumber = index + 1;
1382
- diffLines.push(`+${lineNumber.toString().padStart(4, ' ')}: ${line} [← ${lineNumber}, right]`);
1383
- });
1384
- }
1385
- diffLines.push(``);
1386
- diffLines.push(`📝 **Usage:** addPullRequestInlineComment with position.line = 1 to ${lines.length}`);
1387
- diffLines.push(`💬 **Tip:** All lines in new files can be commented on!`);
1388
- return diffLines.join('\n');
1389
- }
1390
- /**
1391
- * Calculate diff for a deleted file (all content was removed)
1392
- */
1393
- calculateDeletedFileDiff(content, filePath) {
1394
- const lines = content.split('\n');
1395
- if (lines.length === 0) {
1396
- return `--- a${filePath}
1397
- +++ /dev/null
1398
- @@ -1,0 +0,0 @@
1399
- 🗑️ DELETED FILE (Empty)
1400
-
1401
- 💬 INLINE COMMENT LINES: None (empty file was deleted)`;
1402
- }
1403
- // For large files, show a summary instead of all content
1404
- const maxLinesToShow = 100;
1405
- const isLargeFile = lines.length > maxLinesToShow;
1406
- let diffLines = [];
1407
- diffLines.push(`--- a${filePath}`);
1408
- diffLines.push(`+++ /dev/null`);
1409
- diffLines.push(`@@ -1,${lines.length} +0,0 @@`);
1410
- diffLines.push(`🗑️ DELETED FILE (${lines.length} lines removed)`);
1411
- diffLines.push(`💬 INLINE COMMENT LINES: 1 to ${lines.length} (comment on deleted content)`);
1412
- diffLines.push(``);
1413
- if (isLargeFile) {
1414
- // Show first 50 lines with clear line numbers
1415
- const firstLines = lines.slice(0, 50);
1416
- const lastLines = lines.slice(-10);
1417
- const hiddenLineCount = lines.length - firstLines.length - lastLines.length;
1418
- // Add first 50 lines with line numbers
1419
- firstLines.forEach((line, index) => {
1420
- const lineNumber = index + 1;
1421
- diffLines.push(`-${lineNumber.toString().padStart(4, ' ')}: ${line} [← ${lineNumber}, left]`);
1422
- });
1423
- // Add summary of hidden content
1424
- if (hiddenLineCount > 0) {
1425
- const startHidden = firstLines.length + 1;
1426
- const endHidden = lines.length - lastLines.length;
1427
- diffLines.push(`-... (${hiddenLineCount} more deleted lines: ${startHidden}-${endHidden}, all commentable) ...`);
1428
- }
1429
- // Add last 10 lines with line numbers
1430
- lastLines.forEach((line, index) => {
1431
- const lineNumber = lines.length - lastLines.length + index + 1;
1432
- diffLines.push(`-${lineNumber.toString().padStart(4, ' ')}: ${line} [← ${lineNumber}, left]`);
1433
- });
1434
- }
1435
- else {
1436
- // Show all content for smaller files with line numbers
1437
- lines.forEach((line, index) => {
1438
- const lineNumber = index + 1;
1439
- diffLines.push(`-${lineNumber.toString().padStart(4, ' ')}: ${line} [← ${lineNumber}, left]`);
1440
- });
1441
- }
1442
- diffLines.push(``);
1443
- diffLines.push(`📝 **Usage:** addPullRequestInlineComment with position.line = 1 to ${lines.length} (original line numbers)`);
1444
- diffLines.push(`💬 **Tip:** Comment on the original content before it was deleted!`);
1445
- return diffLines.join('\n');
1446
- }
1447
- /**
1448
- * Add inline comment guidance to diff content with accurate line numbers
1449
- */
1450
- addInlineCommentGuidance(diffContent, changeType) {
1451
- // Simply return the clean diff content without verbose annotations
1452
- // The diff itself is self-explanatory with standard +/- markers
1453
- return diffContent;
1454
- }
1455
- // ── New PR Enhancement Methods ─────────────────────────────────
1456
- /**
1457
- * Update pull request properties (title, description, status, auto-complete, draft).
1458
- */
1459
- async updatePullRequest(params) {
1460
- const gitApi = await this.getGitApi();
1461
- const project = params.project || this.config.project;
1462
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1463
- const updatePayload = {};
1464
- if (params.title !== undefined)
1465
- updatePayload.title = params.title;
1466
- if (params.description !== undefined)
1467
- updatePayload.description = params.description;
1468
- if (params.isDraft !== undefined)
1469
- updatePayload.isDraft = params.isDraft;
1470
- if (params.targetRefName !== undefined)
1471
- updatePayload.targetRefName = params.targetRefName;
1472
- if (params.status !== undefined) {
1473
- const statusMap = { 'active': 1, 'abandoned': 3, 'completed': 2 };
1474
- updatePayload.status = statusMap[params.status] || 1;
1475
- }
1476
- // Auto-complete configuration
1477
- if (params.autoCompleteSetBy !== undefined || params.mergeStrategy !== undefined || params.deleteSourceBranch !== undefined) {
1478
- if (params.autoCompleteSetBy) {
1479
- // Set auto-complete with completion options
1480
- updatePayload.autoCompleteSetBy = { id: params.autoCompleteSetBy };
1481
- }
1482
- const completionOptions = {};
1483
- if (params.mergeStrategy) {
1484
- const mergeMap = {
1485
- 'noFastForward': 1, 'squash': 2, 'rebase': 3, 'rebaseMerge': 4,
1486
- };
1487
- completionOptions.mergeStrategy = mergeMap[params.mergeStrategy] || 1;
1488
- }
1489
- if (params.deleteSourceBranch !== undefined) {
1490
- completionOptions.deleteSourceBranch = params.deleteSourceBranch;
1491
- }
1492
- if (Object.keys(completionOptions).length > 0) {
1493
- updatePayload.completionOptions = completionOptions;
1494
- }
1495
- }
1496
- return await gitApi.updatePullRequest(updatePayload, repositoryId, params.pullRequestId, project);
1497
- }
1498
- /**
1499
- * Add or remove reviewers on a pull request.
1500
- */
1501
- async updatePullRequestReviewers(params) {
1502
- const gitApi = await this.getGitApi();
1503
- const project = params.project || this.config.project;
1504
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1505
- const results = { added: [], removed: [] };
1506
- if (params.reviewersToAdd && params.reviewersToAdd.length > 0) {
1507
- for (const reviewer of params.reviewersToAdd) {
1508
- const reviewerObj = {
1509
- id: reviewer,
1510
- isRequired: params.makeRequired || false,
1511
- };
1512
- const result = await gitApi.createPullRequestReviewer(reviewerObj, repositoryId, params.pullRequestId, reviewer, project);
1513
- results.added.push(result);
1514
- }
1515
- }
1516
- if (params.reviewersToRemove && params.reviewersToRemove.length > 0) {
1517
- for (const reviewer of params.reviewersToRemove) {
1518
- await gitApi.deletePullRequestReviewer(repositoryId, params.pullRequestId, reviewer, project);
1519
- results.removed.push(reviewer);
1520
- }
1521
- }
1522
- return results;
1523
- }
1524
- /**
1525
- * Reply to an existing comment thread on a PR.
1526
- */
1527
- async replyToComment(params) {
1528
- const gitApi = await this.getGitApi();
1529
- const project = params.project || this.config.project;
1530
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1531
- // PR comments are markdown-native — normalise literal \n escapes from LLM tool calls.
1532
- const commentContent = params.format === 'html'
1533
- ? params.comment
1534
- : (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.comment));
1535
- const comment = {
1536
- content: commentContent,
1537
- parentCommentId: 0, // 0 = reply to thread
1538
- commentType: 1, // text comment
1539
- };
1540
- return await gitApi.createComment(comment, repositoryId, params.pullRequestId, params.threadId, project);
1541
- }
1542
- /**
1543
- * Update a comment thread's status (resolve/reactivate).
1544
- */
1545
- async updatePullRequestThread(params) {
1546
- const gitApi = await this.getGitApi();
1547
- const project = params.project || this.config.project;
1548
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1549
- const statusMap = {
1550
- 'active': 1,
1551
- 'fixed': 2,
1552
- 'wontFix': 3,
1553
- 'closed': 4,
1554
- 'byDesign': 5,
1555
- 'pending': 6,
1556
- 'unknown': 0,
1557
- };
1558
- const threadUpdate = {
1559
- status: statusMap[params.status] ?? 1,
1560
- };
1561
- return await gitApi.updateThread(threadUpdate, repositoryId, params.pullRequestId, params.threadId, project);
1562
- }
1563
- /**
1564
- * Create a new branch from a source ref.
1565
- */
1566
- async createBranch(params) {
1567
- const gitApi = await this.getGitApi();
1568
- const project = params.project || this.config.project;
1569
- const repositoryId = await this.resolveRepositoryId(params.repository, project);
1570
- // Resolve source ref to a commit ID
1571
- let sourceObjectId = params.sourceRef;
1572
- // If sourceRef looks like a branch name, resolve it to commit ID
1573
- if (!sourceObjectId.match(/^[0-9a-f]{40}$/i)) {
1574
- const branchName = sourceObjectId.replace(/^refs\/heads\//, '');
1575
- const branches = await gitApi.getBranches(repositoryId, project);
1576
- const branch = branches?.find((b) => b.name === branchName || b.name === `refs/heads/${branchName}`);
1577
- if (!branch || !branch.commit?.commitId) {
1578
- throw new Error(`Source branch '${branchName}' not found or has no commits.`);
1579
- }
1580
- sourceObjectId = branch.commit.commitId;
1581
- }
1582
- const branchRef = params.branchName.startsWith('refs/heads/')
1583
- ? params.branchName
1584
- : `refs/heads/${params.branchName}`;
1585
- const refUpdate = [{
1586
- name: branchRef,
1587
- oldObjectId: '0000000000000000000000000000000000000000',
1588
- newObjectId: sourceObjectId,
1589
- }];
1590
- const result = await gitApi.updateRefs(refUpdate, repositoryId, project);
1591
- return result?.[0];
1592
- }
1593
- }
1594
- exports.GitService = GitService;
1595
- //# sourceMappingURL=GitService.js.map