@achieveai/azuredevops-mcp 1.0.0

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