@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.
- package/LICENSE +21 -0
- package/README.md +478 -0
- package/dist/Interfaces/AIAssisted.js +3 -0
- package/dist/Interfaces/AIAssisted.js.map +1 -0
- package/dist/Interfaces/ArtifactManagement.js +3 -0
- package/dist/Interfaces/ArtifactManagement.js.map +1 -0
- package/dist/Interfaces/AzureDevOps.js +3 -0
- package/dist/Interfaces/AzureDevOps.js.map +1 -0
- package/dist/Interfaces/BoardsAndSprints.js +3 -0
- package/dist/Interfaces/BoardsAndSprints.js.map +1 -0
- package/dist/Interfaces/CodeAndRepositories.js +3 -0
- package/dist/Interfaces/CodeAndRepositories.js.map +1 -0
- package/dist/Interfaces/Common.js +67 -0
- package/dist/Interfaces/Common.js.map +1 -0
- package/dist/Interfaces/CostResourceManagement.js +3 -0
- package/dist/Interfaces/CostResourceManagement.js.map +1 -0
- package/dist/Interfaces/DevSecOps.js +3 -0
- package/dist/Interfaces/DevSecOps.js.map +1 -0
- package/dist/Interfaces/ExternalIntegrations.js +3 -0
- package/dist/Interfaces/ExternalIntegrations.js.map +1 -0
- package/dist/Interfaces/HybridCrossPlatform.js +3 -0
- package/dist/Interfaces/HybridCrossPlatform.js.map +1 -0
- package/dist/Interfaces/ProjectManagement.js +3 -0
- package/dist/Interfaces/ProjectManagement.js.map +1 -0
- package/dist/Interfaces/TestingCapabilities.js +3 -0
- package/dist/Interfaces/TestingCapabilities.js.map +1 -0
- package/dist/Interfaces/WorkItems.js +3 -0
- package/dist/Interfaces/WorkItems.js.map +1 -0
- package/dist/Services/AIAssistedDevelopmentService.js +195 -0
- package/dist/Services/AIAssistedDevelopmentService.js.map +1 -0
- package/dist/Services/ArtifactManagementService.js +346 -0
- package/dist/Services/ArtifactManagementService.js.map +1 -0
- package/dist/Services/AzureDevOpsService.js +137 -0
- package/dist/Services/AzureDevOpsService.js.map +1 -0
- package/dist/Services/BoardsSprintsService.js +247 -0
- package/dist/Services/BoardsSprintsService.js.map +1 -0
- package/dist/Services/DevSecOpsService.js +307 -0
- package/dist/Services/DevSecOpsService.js.map +1 -0
- package/dist/Services/EntraAuthHandler.js +85 -0
- package/dist/Services/EntraAuthHandler.js.map +1 -0
- package/dist/Services/GitService.js +1331 -0
- package/dist/Services/GitService.js.map +1 -0
- package/dist/Services/ProjectService.js +233 -0
- package/dist/Services/ProjectService.js.map +1 -0
- package/dist/Services/TestingCapabilitiesService.js +149 -0
- package/dist/Services/TestingCapabilitiesService.js.map +1 -0
- package/dist/Services/WorkItemService.js +522 -0
- package/dist/Services/WorkItemService.js.map +1 -0
- package/dist/Tools/AIAssistedDevelopmentTools.js +137 -0
- package/dist/Tools/AIAssistedDevelopmentTools.js.map +1 -0
- package/dist/Tools/ArtifactManagementTools.js +140 -0
- package/dist/Tools/ArtifactManagementTools.js.map +1 -0
- package/dist/Tools/BoardsSprintsTools.js +149 -0
- package/dist/Tools/BoardsSprintsTools.js.map +1 -0
- package/dist/Tools/DevSecOpsTools.js +147 -0
- package/dist/Tools/DevSecOpsTools.js.map +1 -0
- package/dist/Tools/GitTools.js +1102 -0
- package/dist/Tools/GitTools.js.map +1 -0
- package/dist/Tools/ProjectTools.js +147 -0
- package/dist/Tools/ProjectTools.js.map +1 -0
- package/dist/Tools/TestingCapabilitiesTools.js +157 -0
- package/dist/Tools/TestingCapabilitiesTools.js.map +1 -0
- package/dist/Tools/WorkItemTools.js +532 -0
- package/dist/Tools/WorkItemTools.js.map +1 -0
- package/dist/config.js +149 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +1333 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/getClassMethods.js +8 -0
- package/dist/utils/getClassMethods.js.map +1 -0
- package/dist/utils/repositoryResolver.js +40 -0
- package/dist/utils/repositoryResolver.js.map +1 -0
- 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
|