UNPKG

@mcp-apps/azure-devops-mcp-server

Version:

A Model Context Protocol (MCP) server for Azure DevOps integration

977 lines (843 loc) 39.1 kB
import { getGitApi, getCoreApi } from "../utils/azure-devops-client"; import * as GitInterfaces from "azure-devops-node-api/interfaces/GitInterfaces"; import { PullRequest, PRChanges, PRSummary } from "../models/pr-models"; import { subDays, differenceInHours } from "date-fns"; import { diffLines } from "diff"; export class AzureDevOpsService { /** * Retrieve pull requests from a repository within a date range */ async getPullRequests( organizationUrl: string, project: string, repositoryName: string, days: number = 7, includeActive: boolean = false ): Promise<{ pullRequests: PullRequest[], summary: PRSummary }> { try { // Decode project name if it's URL-encoded const decodedProject = decodeURIComponent(project); if (decodedProject !== project) { console.log(`Project name is URL-encoded. Decoded: ${decodedProject}`); project = decodedProject; } const gitApi = await getGitApi(organizationUrl); const fromDate = subDays(new Date(), days); const searchCriteria = { repositoryId: repositoryName, status: includeActive ? undefined : GitInterfaces.PullRequestStatus.Completed, top: 100 }; const pullRequests = await gitApi.getPullRequests( repositoryName, searchCriteria, project ); if (!pullRequests) { return { pullRequests: [], summary: this.createEmptySummary(fromDate, days) }; } // Filter and transform PRs const filteredPRs = pullRequests .filter((pr: GitInterfaces.GitPullRequest) => { if (!pr.creationDate) return false; const prDate = new Date(pr.creationDate); return prDate >= fromDate; }) .map((pr: GitInterfaces.GitPullRequest) => this.transformPullRequest(pr)); const summary = this.createSummary(filteredPRs, fromDate, days); return { pullRequests: filteredPRs, summary }; } catch (error) { console.error("Failed to retrieve pull requests:", error); // Provide more specific error messages let errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("token") || errorMessage.includes("authentication") || errorMessage.includes("401")) { errorMessage = "Authentication failed. Please ensure you're logged into Azure CLI (run 'az login') or check your Azure DevOps access permissions."; } else if (errorMessage.includes("404") || errorMessage.includes("not found")) { errorMessage = `Repository '${repositoryName}' not found in project '${project}'. Please check the repository and project names.`; } else if (errorMessage.includes("403") || errorMessage.includes("forbidden")) { errorMessage = `Access denied to repository '${repositoryName}'. Please check your permissions in Azure DevOps.`; } throw new Error(`Failed to retrieve pull requests: ${errorMessage}`); } } /** * Get detailed changes for a specific pull request */ async getPRChanges( organizationUrl: string, project: string, repositoryName: string, pullRequestId: number ): Promise<PRChanges> { try { // Decode project name if it's URL-encoded const decodedProject = decodeURIComponent(project); if (decodedProject !== project) { console.log(`Project name is URL-encoded. Decoded: ${decodedProject}`); project = decodedProject; } const gitApi = await getGitApi(organizationUrl); // Get PR iterations to find changes const iterations = await gitApi.getPullRequestIterations( repositoryName, pullRequestId, project ); if (!iterations || iterations.length === 0) { throw new Error(`No iterations found for PR ${pullRequestId}`); } // Get changes for the latest iteration const latestIteration = iterations[iterations.length - 1]; const changes = await gitApi.getPullRequestIterationChanges( repositoryName, pullRequestId, latestIteration.id!, project ); return this.analyzePRChanges(changes); } catch (error) { throw new Error(`Failed to retrieve PR changes: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get code diffs for a pull request with actual diff content */ async getPRDiffs( organizationUrl: string, project: string, repositoryName: string, pullRequestId: number, maxFiles: number = 50 ): Promise<any[]> { try { const gitApi = await getGitApi(organizationUrl); // Get PR details first const pr = await gitApi.getPullRequest(repositoryName, pullRequestId, project); if (!pr) { throw new Error(`Pull request ${pullRequestId} not found`); } // Get file changes from all iterations const iterations = await gitApi.getPullRequestIterations(repositoryName, pullRequestId, project); let allFileChanges: any[] = []; for (let i = 0; i < iterations.length; i++) { const iterationNumber = i + 1; const changes = await gitApi.getPullRequestIterationChanges(repositoryName, pullRequestId, iterationNumber, project); const fileChanges = changes?.changeEntries || []; // Add iteration info to track latest changes fileChanges.forEach((change: any) => { change._iteration = iterationNumber; }); allFileChanges = allFileChanges.concat(fileChanges); } // Remove duplicates (keep latest iteration of each file) const fileChangeMap = new Map(); allFileChanges.forEach(change => { const path = change.item?.path; if (path) { if (!fileChangeMap.has(path) || change._iteration > fileChangeMap.get(path)._iteration) { fileChangeMap.set(path, change); } } }); const fileChanges = Array.from(fileChangeMap.values()); // Limit to maxFiles const limitedChanges = fileChanges.slice(0, maxFiles); // Fetch actual diff content for each file const diffsWithContent = await Promise.all( limitedChanges.map(async (change) => { const baseDiff = { path: change.item?.path, changeType: this.getChangeTypeString(change.changeType), changeTypeCode: change.changeType, iteration: change._iteration, isAdded: change.changeType === 1, isModified: change.changeType === 2, isDeleted: change.changeType === 4, isRenamed: change.changeType === 8, objectId: change.item?.objectId, gitObjectType: change.item?.gitObjectType, url: change.item?.url }; // Skip diff content for deleted files or binary files if (change.changeType === 4 || this.isBinaryFile(change.item?.path)) { return { ...baseDiff, diffContent: null, reason: change.changeType === 4 ? 'File deleted' : 'Binary file' }; } try { // Get diff content using commit IDs from the PR const diffContent = await this.getFileDiffContent( gitApi, repositoryName, project, pr.lastMergeSourceCommit?.commitId, pr.lastMergeTargetCommit?.commitId, change.item?.path ); return { ...baseDiff, diffContent, linesAdded: this.countAddedLines(diffContent), linesDeleted: this.countDeletedLines(diffContent), linesModified: this.countModifiedLines(diffContent) }; } catch (error) { // If we can't get diff content, still return file info return { ...baseDiff, diffContent: null, error: `Could not retrieve diff: ${error instanceof Error ? error.message : String(error)}` }; } }) ); return diffsWithContent; } catch (error) { throw new Error(`Failed to retrieve PR diffs: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get basic information for a specific pull request */ async getPRBasicInfo( organizationUrl: string, project: string, repositoryName: string, pullRequestId: number ) { try { // Decode project name if it's URL-encoded const decodedProject = decodeURIComponent(project); if (decodedProject !== project) { console.log(`Project name is URL-encoded. Decoded: ${decodedProject}`); project = decodedProject; } const gitApi = await getGitApi(organizationUrl); // Get PR details const pr = await gitApi.getPullRequest(repositoryName, pullRequestId, project); if (!pr) { throw new Error(`Pull request ${pullRequestId} not found`); } // Get reviewers const reviewers = await gitApi.getPullRequestReviewers(repositoryName, pullRequestId, project); // Get commits count const commits = await gitApi.getPullRequestCommits(repositoryName, pullRequestId, project); // Get work items linked to this PR const workItemRefs = await gitApi.getPullRequestWorkItemRefs(repositoryName, pullRequestId, project); return { basicInfo: this.transformPullRequest(pr), reviewProcess: { totalReviewers: reviewers?.length || 0, reviewers: reviewers?.map(r => ({ name: r.displayName || r.uniqueName || "Unknown", vote: r.vote || 0, hasDeclined: r.vote === -10, hasApproved: r.vote === 10, hasApprovedWithSuggestions: r.vote === 5, isRequired: r.isRequired || false })) || [], requiredReviewers: reviewers?.filter(r => r.isRequired).length || 0, approvals: reviewers?.filter(r => r.vote === 10).length || 0, rejections: reviewers?.filter(r => r.vote === -10).length || 0 }, commits: { totalCommits: commits?.length || 0, commitShas: commits?.map(c => c.commitId).slice(0, 10) || [] }, linkedWorkItems: { totalWorkItems: workItemRefs?.length || 0, workItemIds: workItemRefs?.map(wi => wi.id).filter(Boolean) || [] }, metadata: { pullRequestUrl: `${organizationUrl}/${project}/_git/${repositoryName}/pullrequest/${pullRequestId}`, repositoryId: pr.repository?.id, lastMergeSourceCommit: pr.lastMergeSourceCommit?.commitId, lastMergeTargetCommit: pr.lastMergeTargetCommit?.commitId } }; } catch (error) { console.error("Failed to retrieve PR basic info:", error); let errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("404") || errorMessage.includes("not found")) { errorMessage = `Pull request ${pullRequestId} not found in repository '${repositoryName}'. Please check the PR ID and repository name.`; } else if (errorMessage.includes("403") || errorMessage.includes("forbidden")) { errorMessage = `Access denied to pull request ${pullRequestId}. Please check your permissions.`; } throw new Error(`Failed to retrieve PR basic info: ${errorMessage}`); } } /** * Analyze test impact and coverage implications of a pull request */ async getPRTestImpact( organizationUrl: string, project: string, repositoryName: string, pullRequestId: number, options: { includeTestFiles?: boolean; analysisDepth?: "basic" | "standard" | "comprehensive"; } = {} ) { try { const { includeTestFiles = true, analysisDepth = "standard" } = options; const gitApi = await getGitApi(organizationUrl); // Get basic PR information const pr = await gitApi.getPullRequest(repositoryName, pullRequestId, project); if (!pr) { throw new Error(`Pull request ${pullRequestId} not found`); } const basicInfo = this.transformPullRequest(pr); // Collect file changes from all iterations const iterations = await gitApi.getPullRequestIterations(repositoryName, pullRequestId, project); let allFileChanges: any[] = []; for (let i = 0; i < iterations.length; i++) { const iterationNumber = i + 1; const changes = await gitApi.getPullRequestIterationChanges(repositoryName, pullRequestId, iterationNumber, project); const fileChanges = changes?.changeEntries || []; fileChanges.forEach((change: any) => { change._iteration = iterationNumber; }); allFileChanges = allFileChanges.concat(fileChanges); } // Remove duplicates (keep latest iteration of each file) const fileChangeMap = new Map(); allFileChanges.forEach(change => { const path = change.item?.path; if (path) { if (!fileChangeMap.has(path) || change._iteration > fileChangeMap.get(path)._iteration) { fileChangeMap.set(path, change); } } }); const fileChanges = Array.from(fileChangeMap.values()); // Perform test impact analysis const testFileAnalysis = this.analyzeTestFiles(fileChanges, includeTestFiles); const codeFileAnalysis = this.analyzeCodeFiles(fileChanges); const coverageAnalysis = this.analyzeCoverageImpact(fileChanges, analysisDepth); const testStrategyAnalysis = this.analyzeTestingStrategy(fileChanges, testFileAnalysis, analysisDepth); // Calculate overall test impact const overallImpact = this.calculateTestImpact({ testFileAnalysis, codeFileAnalysis, coverageAnalysis, testStrategyAnalysis, analysisDepth }); // Generate test recommendations const testRecommendations = this.generateTestRecommendations({ testFileAnalysis, codeFileAnalysis, coverageAnalysis, testStrategyAnalysis, overallImpact, analysisDepth }); // Generate test execution plan const executionPlan = this.generateTestExecutionPlan({ testFileAnalysis, codeFileAnalysis, overallImpact, analysisDepth }); const result = { pullRequestId, repositoryName, basicInfo, testImpactAssessment: { overall: overallImpact, analysis: { testFiles: testFileAnalysis, codeFiles: codeFileAnalysis, coverage: coverageAnalysis, testStrategy: testStrategyAnalysis } }, recommendations: testRecommendations, executionPlan, summary: this.generateTestImpactSummary(overallImpact, testRecommendations), options: { includeTestFiles, analysisDepth }, analyzedAt: new Date().toISOString() }; return result; } catch (error) { console.error("Failed to analyze PR test impact:", error); let errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("404") || errorMessage.includes("not found")) { errorMessage = `Pull request ${pullRequestId} not found in repository '${repositoryName}'. Please check the PR ID and repository name.`; } else if (errorMessage.includes("403") || errorMessage.includes("forbidden")) { errorMessage = `Access denied to repository '${repositoryName}' or project '${project}'. Please verify your permissions.`; } throw new Error(`Failed to analyze PR test impact: ${errorMessage}`); } } /** * Get comprehensive repository context information */ async getRepositoryContext( organizationUrl: string, project: string, repositoryName: string, options: { includeFileStructure?: boolean; includeActivity?: boolean; activityDays?: number; analysisDepth?: "basic" | "standard" | "comprehensive"; } = {} ) { try { // Decode project name if it's URL-encoded const decodedProject = decodeURIComponent(project); if (decodedProject !== project) { console.log(`Project name is URL-encoded. Decoded: ${decodedProject}`); project = decodedProject; } const { includeFileStructure = true, includeActivity = true, activityDays = 30, analysisDepth = "standard" } = options; const gitApi = await getGitApi(organizationUrl); const coreApi = await getCoreApi(organizationUrl); // Get repository information const repository = await gitApi.getRepository(repositoryName, project); if (!repository) { throw new Error(`Repository '${repositoryName}' not found in project '${project}'`); } // Get branches const branches = await gitApi.getBranches(repositoryName, project); // Get project information const projectInfo = await coreApi.getProject(project); // Initialize context object with basic information const context: any = { repositoryName, projectName: project, metadata: this.extractRepositoryMetadata(repository), repository: { id: repository.id, name: repository.name, url: repository.webUrl, defaultBranch: repository.defaultBranch, size: repository.size }, branches: this.analyzeBranches(branches || []), project: { id: projectInfo.id, name: projectInfo.name, description: projectInfo.description, visibility: projectInfo.visibility }, options: { includeFileStructure, includeActivity, activityDays, analysisDepth }, analyzedAt: new Date().toISOString() }; // Add file structure analysis if requested if (includeFileStructure) { context.fileStructure = await this.analyzeFileStructure( gitApi, repositoryName, project, analysisDepth ); } // Add activity analysis if requested if (includeActivity) { context.activity = await this.analyzeRepositoryActivity( gitApi, repositoryName, project, activityDays, analysisDepth ); } // Add comprehensive analysis for comprehensive mode if (analysisDepth === 'comprehensive') { context.comprehensive = await this.performComprehensiveAnalysis( gitApi, repositoryName, project, repository ); } // Generate insights and recommendations context.insights = this.generateRepositoryInsights(context); context.summary = this.generateRepositorySummary(context); return context; } catch (error) { console.error("Failed to retrieve repository context:", error); let errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("404") || errorMessage.includes("not found")) { errorMessage = `Repository '${repositoryName}' not found in project '${project}'. Please check the repository name and project.`; } else if (errorMessage.includes("403") || errorMessage.includes("forbidden")) { errorMessage = `Access denied to repository '${repositoryName}' or project '${project}'. Please verify your permissions.`; } throw new Error(`Failed to retrieve repository context: ${errorMessage}`); } } // Helper methods that need to be implemented private createEmptySummary(fromDate: Date, days: number): PRSummary { return { totalPRs: 0, dateRange: { from: fromDate.toISOString().split('T')[0], to: new Date().toISOString().split('T')[0] }, statusBreakdown: { completed: 0, active: 0, abandoned: 0 } }; } private createSummary(pullRequests: PullRequest[], fromDate: Date, days: number): PRSummary { return { totalPRs: pullRequests.length, dateRange: { from: fromDate.toISOString().split('T')[0], to: new Date().toISOString().split('T')[0] }, statusBreakdown: { completed: pullRequests.filter(pr => pr.status === "completed").length, active: pullRequests.filter(pr => pr.status === "active").length, abandoned: pullRequests.filter(pr => pr.status === "abandoned").length } }; } private transformPullRequest(pr: GitInterfaces.GitPullRequest): PullRequest { return { id: pr.pullRequestId || 0, title: pr.title || "", description: pr.description || "", author: pr.createdBy?.displayName || pr.createdBy?.uniqueName || "Unknown", createdDate: pr.creationDate ? (typeof pr.creationDate === 'string' ? pr.creationDate : pr.creationDate.toISOString()) : "", completedDate: pr.closedDate ? (typeof pr.closedDate === 'string' ? pr.closedDate : pr.closedDate.toISOString()) : undefined, mergedDate: pr.mergeStatus === GitInterfaces.PullRequestAsyncStatus.Succeeded ? (pr.closedDate ? (typeof pr.closedDate === 'string' ? pr.closedDate : pr.closedDate.toISOString()) : undefined) : undefined, targetBranch: pr.targetRefName?.replace("refs/heads/", "") || "", sourceBranch: pr.sourceRefName?.replace("refs/heads/", "") || "", status: this.mapPRStatus(pr.status), mergeStatus: pr.mergeStatus?.toString() || undefined, isDraft: pr.isDraft || false, labels: pr.labels?.map((l: any) => l.name).filter(Boolean) || [] }; } private mapPRStatus(status?: GitInterfaces.PullRequestStatus): string { switch (status) { case GitInterfaces.PullRequestStatus.Active: return "active"; case GitInterfaces.PullRequestStatus.Completed: return "completed"; case GitInterfaces.PullRequestStatus.Abandoned: return "abandoned"; default: return "unknown"; } } private analyzePRChanges(changes: GitInterfaces.GitPullRequestIterationChanges | undefined): PRChanges { if (!changes || !changes.changeEntries) { return { filesChanged: [], totalFilesChanged: 0, linesAdded: 0, linesDeleted: 0, netLinesChanged: 0, changedDirectories: [], fileTypeBreakdown: {}, criticalFilesChanged: [], configurationFiles: [], migrationFiles: [], testFiles: [] }; } const filesChanged = changes.changeEntries.map(entry => entry.item?.path || "").filter(Boolean); const directories = [...new Set(filesChanged.map(file => file.split('/').slice(0, -1).join('/')))]; // Analyze file types const fileTypeBreakdown: Record<string, number> = {}; const criticalFiles: string[] = []; const configFiles: string[] = []; const migrationFiles: string[] = []; const testFiles: string[] = []; filesChanged.forEach(file => { const extension = file.split('.').pop()?.toLowerCase() || ''; fileTypeBreakdown[extension] = (fileTypeBreakdown[extension] || 0) + 1; // Categorize files if (this.isCriticalFile(file)) criticalFiles.push(file); if (this.isConfigurationFile(file)) configFiles.push(file); if (this.isMigrationFile(file)) migrationFiles.push(file); if (this.isTestFile(file)) testFiles.push(file); }); return { filesChanged, totalFilesChanged: filesChanged.length, linesAdded: 0, linesDeleted: 0, netLinesChanged: 0, changedDirectories: directories, fileTypeBreakdown, criticalFilesChanged: criticalFiles, configurationFiles: configFiles, migrationFiles: migrationFiles, testFiles: testFiles }; } private getChangeTypeString(changeType: number): string { switch (changeType) { case 1: return 'add'; case 2: return 'edit'; case 4: return 'delete'; case 8: return 'rename'; default: return 'unknown'; } } private isBinaryFile(filePath: string | undefined): boolean { if (!filePath) return false; const binaryExtensions = ['.exe', '.dll', '.pdf', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.zip', '.tar', '.gz']; return binaryExtensions.some(ext => filePath.toLowerCase().endsWith(ext)); } private async getFileDiffContent( gitApi: any, repositoryName: string, project: string, sourceCommitId: string | undefined, targetCommitId: string | undefined, filePath: string | undefined ): Promise<string | null> { if (!sourceCommitId || !targetCommitId || !filePath) { return null; } try { // Try to get file content from both commits const [sourceContent, targetContent] = await Promise.all([ this.getFileContentAtCommit(gitApi, repositoryName, project, filePath, sourceCommitId), this.getFileContentAtCommit(gitApi, repositoryName, project, filePath, targetCommitId) ]); if (sourceContent === null && targetContent === null) { return null; } if (sourceContent === null && targetContent !== null) { return this.formatAsUnifiedDiff(filePath, '', targetContent, 'added'); } if (sourceContent !== null && targetContent === null) { return this.formatAsUnifiedDiff(filePath, sourceContent, '', 'deleted'); } if (sourceContent !== null && targetContent !== null) { if (sourceContent === targetContent) { return null; } return this.formatAsUnifiedDiff(filePath, targetContent, sourceContent, 'modified'); } return null; } catch (error) { console.warn(`Failed to get diff content for ${filePath}:`, error); return null; } } private async getFileContentAtCommit( gitApi: any, repositoryName: string, project: string, filePath: string, commitId: string ): Promise<string | null> { try { const contentStream = await gitApi.getItemText( repositoryName, filePath, project, undefined, GitInterfaces.VersionControlRecursionType.None, false, false, false, { versionType: GitInterfaces.GitVersionType.Commit, version: commitId }, true, false, true ); if (contentStream) { const content = await this.streamToString(contentStream); return content; } return null; } catch (error) { return null; } } private async streamToString(stream: NodeJS.ReadableStream): Promise<string> { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; stream.on('data', (chunk: Buffer) => { chunks.push(chunk); }); stream.on('end', () => { const result = Buffer.concat(chunks).toString('utf8'); resolve(result); }); stream.on('error', (error) => { reject(error); }); setTimeout(() => { reject(new Error('Stream read timeout')); }, 30000); }); } private formatAsUnifiedDiff( filePath: string, oldContent: string, newContent: string, changeType: 'added' | 'deleted' | 'modified' ): string { const oldLines = oldContent.split('\n'); const newLines = newContent.split('\n'); const diffResult = diffLines(oldContent, newContent, { ignoreWhitespace: false, newlineIsToken: false }); let unifiedDiff = `--- a/${filePath}\n+++ b/${filePath}\n`; if (changeType === 'added') { unifiedDiff += `@@ -0,0 +1,${newLines.length} @@\n`; } else if (changeType === 'deleted') { unifiedDiff += `@@ -1,${oldLines.length} +0,0 @@\n`; } else { unifiedDiff += `@@ -1,${oldLines.length} +1,${newLines.length} @@\n`; } for (const part of diffResult) { const lines = part.value.split('\n'); if (lines[lines.length - 1] === '') { lines.pop(); } for (const line of lines) { if (part.added) { unifiedDiff += `+${line}\n`; } else if (part.removed) { unifiedDiff += `-${line}\n`; } else { unifiedDiff += ` ${line}\n`; } } } return unifiedDiff; } private countAddedLines(diffContent: string | null): number { if (!diffContent) return 0; const lines = diffContent.split('\n'); return lines.filter(line => line.startsWith('+')).length; } private countDeletedLines(diffContent: string | null): number { if (!diffContent) return 0; const lines = diffContent.split('\n'); return lines.filter(line => line.startsWith('-')).length; } private countModifiedLines(diffContent: string | null): number { if (!diffContent) return 0; const added = this.countAddedLines(diffContent); const deleted = this.countDeletedLines(diffContent); return Math.min(added, deleted); } // File classification helpers private isCriticalFile(filePath: string): boolean { const criticalPatterns = [ /dockerfile/i, /package\.json$/i, /requirements\.txt$/i, /web\.config$/i, /app\.config$/i, /appsettings/i, /\.env/i ]; return criticalPatterns.some(pattern => pattern.test(filePath)); } private isConfigurationFile(filePath: string): boolean { const configPatterns = [ /\.config$/i, /\.ini$/i, /\.yaml$/i, /\.yml$/i, /\.toml$/i, /\.properties$/i, /appsettings/i ]; return configPatterns.some(pattern => pattern.test(filePath)); } private isMigrationFile(filePath: string): boolean { const migrationPatterns = [ /migration/i, /migrate/i, /\.sql$/i, /schema/i ]; return migrationPatterns.some(pattern => pattern.test(filePath)); } private isTestFile(filePath: string): boolean { const testPatterns = [ /test/i, /spec/i, /\.test\./i, /\.spec\./i, /__tests__/i ]; return testPatterns.some(pattern => pattern.test(filePath)); } // Helper methods for test analysis private analyzeTestFiles(fileChanges: any[], includeTestFiles: boolean): any { return { totalTestFiles: 0, testFiles: [] }; } private analyzeCodeFiles(fileChanges: any[]): any { return { totalCodeFiles: 0, codeFiles: [] }; } private analyzeCoverageImpact(fileChanges: any[], analysisDepth: string): any { return { estimatedCoverageImpact: 'unknown' }; } private analyzeTestingStrategy(fileChanges: any[], testFileAnalysis: any, analysisDepth: string): any { return { strategy: 'unknown' }; } private calculateTestImpact(data: any): any { return { impact: 'low' }; } private generateTestRecommendations(data: any): any[] { return []; } private generateTestExecutionPlan(data: any): any { return { plan: 'execute all tests' }; } private generateTestImpactSummary(overallImpact: any, testRecommendations: any[]): any { return { summary: 'Low impact' }; } private extractRepositoryMetadata(repository: any): any { return { name: repository.name }; } private analyzeBranches(branches: any[]): any { return { totalBranches: branches.length }; } private async analyzeFileStructure(gitApi: any, repositoryName: string, project: string, analysisDepth: string): Promise<any> { return { structure: 'unknown' }; } private async analyzeRepositoryActivity(gitApi: any, repositoryName: string, project: string, activityDays: number, analysisDepth: string): Promise<any> { return { activity: 'unknown' }; } private async performComprehensiveAnalysis(gitApi: any, repositoryName: string, project: string, repository: any): Promise<any> { return { comprehensive: 'unknown' }; } private generateRepositoryInsights(context: any): any { return { insights: [] }; } private generateRepositorySummary(context: any): any { return { summary: 'Repository analysis complete' }; } }