UNPKG

@mcp-consultant-tools/github-enterprise

Version:

MCP server for GitHub Enterprise - repositories, commits, pull requests, and code search

1,101 lines 42.5 kB
import { Octokit } from '@octokit/rest'; import { createAppAuth } from '@octokit/auth-app'; import axios from 'axios'; import { auditLogger } from '@mcp-consultant-tools/core'; /** * GitHub Enterprise Service * Manages authentication, API requests, caching, and branch selection for GitHub Enterprise Cloud */ export class GitHubEnterpriseService { config; baseApiUrl; octokit = null; // Token caching (for GitHub App) accessToken = null; tokenExpirationTime = 0; // Response caching cache = new Map(); constructor(config) { this.config = config; this.baseApiUrl = `${config.baseUrl}/api/v3`; // Initialize Octokit based on auth method this.initializeOctokit(); } /** * Initialize Octokit client based on authentication method */ initializeOctokit() { try { if (this.config.authMethod === 'pat') { // PAT authentication (primary method) this.octokit = new Octokit({ auth: this.config.pat, baseUrl: this.baseApiUrl, userAgent: 'mcp-consultant-tools', }); } else if (this.config.authMethod === 'github-app') { // GitHub App authentication (optional/advanced) if (!this.config.appId || !this.config.appPrivateKey || !this.config.appInstallationId) { throw new Error('GitHub App authentication requires appId, appPrivateKey, and appInstallationId'); } this.octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId: this.config.appId, privateKey: this.config.appPrivateKey, installationId: this.config.appInstallationId, }, baseUrl: this.baseApiUrl, userAgent: 'mcp-consultant-tools', }); } else { throw new Error(`Unsupported authentication method: ${this.config.authMethod}`); } } catch (error) { console.error('Failed to initialize Octokit:', error.message); throw error; } } /** * Get access token with caching (for GitHub App auth) * Implements 5-minute buffer pattern before expiry */ async getAccessToken() { if (this.config.authMethod === 'pat') { return this.config.pat; } const currentTime = Date.now(); // Return cached token if still valid (with 5 minute buffer) if (this.accessToken && this.tokenExpirationTime > currentTime) { return this.accessToken; } // Acquire new token for GitHub App try { const auth = await this.octokit.auth({ type: 'installation' }); if (!auth.token) { throw new Error('GitHub App auth did not return a token'); } const token = auth.token; this.accessToken = token; // GitHub App installation tokens expire after 1 hour // Set expiration time (subtract 5 minutes to refresh early) this.tokenExpirationTime = currentTime + (55 * 60 * 1000); // 55 minutes return token; } catch (error) { console.error('Failed to acquire GitHub App installation token:', error.message); throw new Error(`Failed to acquire GitHub App token: ${error.message}`); } } /** * Get cache key for a request */ getCacheKey(method, repo, resource, params) { const paramStr = params ? JSON.stringify(params) : ''; return `${method}:${repo}:${resource}:${paramStr}`; } /** * Get cached response */ getCached(key) { const cached = this.cache.get(key); if (cached && Date.now() < cached.expires) { return cached.data; } this.cache.delete(key); // Expired - remove it return null; } /** * Set cache entry */ setCache(key, data, ttlSeconds) { if (!this.config.enableCache) return; const ttl = ttlSeconds || this.config.cacheTtl; this.cache.set(key, { data, expires: Date.now() + (ttl * 1000) }); } /** * Clear cache entries * @param pattern Optional pattern to match cache keys * @param repoId Optional repo ID to clear cache for specific repo * @returns Number of cache entries cleared */ clearCache(pattern, repoId) { if (repoId) { const repo = this.getRepoById(repoId); const repoPattern = `${repo.owner}/${repo.repo}`; pattern = pattern ? `${repoPattern}:${pattern}` : repoPattern; } if (pattern) { let cleared = 0; for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key); cleared++; } } console.error(`Cleared ${cleared} cache entries matching pattern '${pattern}'`); return cleared; } const size = this.cache.size; this.cache.clear(); console.error(`Cleared all ${size} cache entries`); return size; } /** * Make API request with error handling and caching */ async makeRequest(endpoint, options = {}) { const { method = 'GET', data, useCache = true, cacheTtl, repoId } = options; // Check cache for GET requests if (method === 'GET' && useCache && this.config.enableCache) { const cacheKey = this.getCacheKey(method, repoId || '', endpoint, data); const cached = this.getCached(cacheKey); if (cached) { return cached; } } try { const token = await this.getAccessToken(); const url = endpoint.startsWith('http') ? endpoint : `${this.baseApiUrl}/${endpoint}`; const response = await axios({ method, url, headers: { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json', 'X-GitHub-Api-Version': this.config.apiVersion, 'Content-Type': 'application/json', }, data, }); // Cache successful GET responses if (method === 'GET' && useCache && this.config.enableCache) { const cacheKey = this.getCacheKey(method, repoId || '', endpoint, data); this.setCache(cacheKey, response.data, cacheTtl); } return response.data; } catch (error) { // Comprehensive error handling let errorMessage = 'Unknown error'; let errorDetails = {}; if (error.response) { const status = error.response.status; const data = error.response.data; switch (status) { case 401: errorMessage = 'Authentication failed. Check your PAT or GitHub App credentials.'; break; case 403: if (error.response.headers['x-ratelimit-remaining'] === '0') { const resetTime = error.response.headers['x-ratelimit-reset']; const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleString() : 'unknown'; errorMessage = `Rate limit exceeded. Resets at ${resetDate}.`; } else { errorMessage = 'Access denied. Check repository permissions.'; } break; case 404: errorMessage = `Resource not found: ${endpoint}`; break; case 422: errorMessage = `Validation failed: ${data?.message || 'Invalid request parameters'}`; break; default: errorMessage = `HTTP ${status}: ${data?.message || error.message}`; } errorDetails = { status, message: data?.message }; } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { errorMessage = `Network error: Unable to reach GitHub Enterprise at ${this.config.baseUrl}. Check your connection and GHE_URL.`; } else if (error.code === 'ETIMEDOUT') { errorMessage = 'Request timeout. GitHub Enterprise API is slow to respond.'; } else { errorMessage = error.message; } console.error('GitHub Enterprise API request failed:', { endpoint, method, status: error.response?.status, error: errorMessage }); throw new Error(errorMessage); } } /** * Get all configured repositories */ getAllRepos() { return this.config.repos; } /** * Get active repositories only */ getActiveRepos() { return this.config.repos.filter(r => r.active); } /** * Get repository by ID with validation */ getRepoById(repoId) { const repo = this.config.repos.find(r => r.id === repoId); if (!repo) { const availableIds = this.config.repos.map(r => r.id).join(', '); throw new Error(`Repository '${repoId}' not found. Available repositories: ${availableIds || 'none'}`); } if (!repo.active) { throw new Error(`Repository '${repoId}' is inactive. Set 'active: true' in configuration to enable it.`); } return repo; } /** * List all branches for a repository */ async listBranches(repoId, protectedOnly) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const branches = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/branches`, { repoId }); const filteredBranches = protectedOnly !== undefined ? branches.filter(b => b.protected === protectedOnly) : branches; auditLogger.log({ operation: 'list-branches', operationType: 'READ', componentType: 'Branch', success: true, parameters: { repoId, protectedOnly }, executionTimeMs: timer(), }); return filteredBranches; } catch (error) { auditLogger.log({ operation: 'list-branches', operationType: 'READ', componentType: 'Branch', success: false, error: error.message, parameters: { repoId }, executionTimeMs: timer(), }); throw error; } } /** * Auto-detect default branch for a repository * Handles typos gracefully and provides alternatives */ async getDefaultBranch(repoId, userSpecified) { const repo = this.getRepoById(repoId); // 1. User explicitly specified branch (highest priority) if (userSpecified) { const branches = await this.listBranches(repoId); const exists = branches.find(b => b.name === userSpecified); if (exists) { return { branch: userSpecified, reason: 'user-specified', confidence: 'high' }; } // Branch doesn't exist - show available branches const availableBranches = branches.map(b => ` - ${b.name}`).join('\n'); throw new Error(`Branch "${userSpecified}" not found in ${repo.owner}/${repo.repo}.\n\n` + `Available branches:\n${availableBranches}`); } // 2. Check if default branch configured for this repo if (repo.defaultBranch) { return { branch: repo.defaultBranch, reason: 'configured default', confidence: 'high' }; } // 3. Get all branches const branches = await this.listBranches(repoId); // 4. Filter and sort release branches (handle typos gracefully) const releaseBranches = branches .filter(b => b.name.toLowerCase().startsWith('release/')) // Case-insensitive .map(b => { // Parse version number after "release/" const versionStr = b.name.substring(b.name.indexOf('/') + 1); const version = parseFloat(versionStr); return { name: b.name, version: isNaN(version) ? 0 : version, raw: versionStr }; }) .filter(b => b.version > 0) // Only keep valid version numbers .sort((a, b) => b.version - a.version); // Highest first // 5. Auto-select highest version, but ALWAYS show alternatives if (releaseBranches.length > 0) { const selected = releaseBranches[0].name; const allAlternatives = releaseBranches.slice(1).map(b => b.name); console.error(`✓ Auto-selected branch: ${selected} (highest release version ${releaseBranches[0].version})`); if (allAlternatives.length > 0) { console.error(` Alternatives: ${allAlternatives.slice(0, 3).join(', ')}${allAlternatives.length > 3 ? '...' : ''}`); } return { branch: selected, reason: `auto-detected: highest release version (${releaseBranches[0].version})`, confidence: 'medium', alternatives: allAlternatives, message: `Auto-selected "${selected}". If this is incorrect, specify a different branch explicitly.` }; } // 6. No release branches found - fallback to main/master console.error(`⚠️ No release branches found in format "release/X.Y" for ${repo.owner}/${repo.repo}`); const availableBranchNames = branches.map(b => b.name); console.error(` Available branches: ${availableBranchNames.slice(0, 5).join(', ')}${availableBranchNames.length > 5 ? '...' : ''}`); const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master'); if (mainBranch) { console.error(`⚠️ Falling back to: ${mainBranch.name} (main branch - likely production)`); return { branch: mainBranch.name, reason: 'fallback to main branch (no release branches found)', confidence: 'low', alternatives: availableBranchNames.filter(n => n !== mainBranch.name), message: `No release branches found. Using "${mainBranch.name}" as fallback. User should verify this is correct.` }; } // 7. Cannot determine - list all branches and throw error const branchList = availableBranchNames.map(n => ` - ${n}`).join('\n'); throw new Error(`Could not determine default branch for ${repo.owner}/${repo.repo}.\n\n` + `Available branches:\n${branchList}\n\n` + `Please specify a branch explicitly or configure a defaultBranch in GHE_REPOS.`); } /** * Get file content from a repository */ async getFile(repoId, path, branch) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { // Auto-detect branch if not specified const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch; const file = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}?ref=${selectedBranch}`, { repoId }); // Check file size if (file.size > this.config.maxFileSize) { throw new Error(`File size (${file.size} bytes) exceeds maximum allowed size (${this.config.maxFileSize} bytes). ` + `Increase GHE_MAX_FILE_SIZE if needed.`); } // Decode base64 content if (file.encoding === 'base64') { file.decodedContent = Buffer.from(file.content, 'base64').toString('utf-8'); } auditLogger.log({ operation: 'get-file', operationType: 'READ', componentType: 'File', componentName: path, success: true, parameters: { repoId, path, branch: selectedBranch }, executionTimeMs: timer(), }); return { ...file, branch: selectedBranch }; } catch (error) { auditLogger.log({ operation: 'get-file', operationType: 'READ', componentType: 'File', componentName: path, success: false, error: error.message, parameters: { repoId, path, branch }, executionTimeMs: timer(), }); throw error; } } /** * Search code across repositories */ async searchCode(query, repoId, path, extension) { const timer = auditLogger.startTimer(); try { // Build search query let searchQuery = query; if (repoId) { const repo = this.getRepoById(repoId); searchQuery += ` repo:${repo.owner}/${repo.repo}`; } if (path) { searchQuery += ` path:${path}`; } if (extension) { searchQuery += ` extension:${extension}`; } const result = await this.makeRequest(`search/code?q=${encodeURIComponent(searchQuery)}&per_page=${this.config.maxSearchResults}`, { useCache: false } // Don't cache search results ); auditLogger.log({ operation: 'search-code', operationType: 'READ', componentType: 'Code', success: true, parameters: { query, repoId, path, extension, totalResults: result.total_count }, executionTimeMs: timer(), }); return result; } catch (error) { auditLogger.log({ operation: 'search-code', operationType: 'READ', componentType: 'Code', success: false, error: error.message, parameters: { query, repoId, path, extension }, executionTimeMs: timer(), }); throw error; } } /** * List files in a directory */ async listFiles(repoId, path, branch) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { // Auto-detect branch if not specified const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch; const dirPath = path || ''; const contents = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${dirPath}?ref=${selectedBranch}`, { repoId }); auditLogger.log({ operation: 'list-files', operationType: 'READ', componentType: 'Directory', componentName: path || '/', success: true, parameters: { repoId, path, branch: selectedBranch }, executionTimeMs: timer(), }); return { contents, branch: selectedBranch }; } catch (error) { auditLogger.log({ operation: 'list-files', operationType: 'READ', componentType: 'Directory', componentName: path || '/', success: false, error: error.message, parameters: { repoId, path, branch }, executionTimeMs: timer(), }); throw error; } } /** * Get commit history for a branch */ async getCommits(repoId, branch, since, until, author, path, limit = 50) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { // Auto-detect branch if not specified const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch; // Build query parameters const params = { sha: selectedBranch, per_page: limit, }; if (since) params.since = since; if (until) params.until = until; if (author) params.author = author; if (path) params.path = path; const queryString = new URLSearchParams(params).toString(); const commits = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/commits?${queryString}`, { repoId }); auditLogger.log({ operation: 'get-commits', operationType: 'READ', componentType: 'Commit', success: true, parameters: { repoId, branch: selectedBranch, since, until, author, path, limit, count: commits.length }, executionTimeMs: timer(), }); return commits; } catch (error) { auditLogger.log({ operation: 'get-commits', operationType: 'READ', componentType: 'Commit', success: false, error: error.message, parameters: { repoId, branch, since, until, author, path, limit }, executionTimeMs: timer(), }); throw error; } } /** * Get commit details */ async getCommitDetails(repoId, sha) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const commit = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/commits/${sha}`, { repoId }); auditLogger.log({ operation: 'get-commit-details', operationType: 'READ', componentType: 'Commit', componentId: sha, success: true, parameters: { repoId, sha }, executionTimeMs: timer(), }); return commit; } catch (error) { auditLogger.log({ operation: 'get-commit-details', operationType: 'READ', componentType: 'Commit', componentId: sha, success: false, error: error.message, parameters: { repoId, sha }, executionTimeMs: timer(), }); throw error; } } /** * Search commits by message */ async searchCommits(query, repoId, author, since, until) { const timer = auditLogger.startTimer(); try { // Build search query let searchQuery = query; if (repoId) { const repo = this.getRepoById(repoId); searchQuery += ` repo:${repo.owner}/${repo.repo}`; } if (author) { searchQuery += ` author:${author}`; } if (since) { searchQuery += ` committer-date:>=${since}`; } if (until) { searchQuery += ` committer-date:<=${until}`; } const result = await this.makeRequest(`search/commits?q=${encodeURIComponent(searchQuery)}`, { useCache: false } // Don't cache search results ); auditLogger.log({ operation: 'search-commits', operationType: 'READ', componentType: 'Commit', success: true, parameters: { query, repoId, author, since, until, totalResults: result.total_count }, executionTimeMs: timer(), }); return result; } catch (error) { auditLogger.log({ operation: 'search-commits', operationType: 'READ', componentType: 'Commit', success: false, error: error.message, parameters: { query, repoId, author, since, until }, executionTimeMs: timer(), }); throw error; } } /** * Compare two branches */ async compareBranches(repoId, base, head) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const comparison = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/compare/${base}...${head}`, { repoId }); auditLogger.log({ operation: 'compare-branches', operationType: 'READ', componentType: 'Branch', success: true, parameters: { repoId, base, head, aheadBy: comparison.ahead_by, behindBy: comparison.behind_by }, executionTimeMs: timer(), }); return comparison; } catch (error) { auditLogger.log({ operation: 'compare-branches', operationType: 'READ', componentType: 'Branch', success: false, error: error.message, parameters: { repoId, base, head }, executionTimeMs: timer(), }); throw error; } } /** * Get branch details */ async getBranchDetails(repoId, branch) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const branchInfo = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/branches/${branch}`, { repoId }); auditLogger.log({ operation: 'get-branch-details', operationType: 'READ', componentType: 'Branch', componentName: branch, success: true, parameters: { repoId, branch }, executionTimeMs: timer(), }); return branchInfo; } catch (error) { auditLogger.log({ operation: 'get-branch-details', operationType: 'READ', componentType: 'Branch', componentName: branch, success: false, error: error.message, parameters: { repoId, branch }, executionTimeMs: timer(), }); throw error; } } /** * List pull requests */ async listPullRequests(repoId, state = 'open', base, head, sort = 'created', limit = 30) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const params = { state, sort, per_page: limit, }; if (base) params.base = base; if (head) params.head = head; const queryString = new URLSearchParams(params).toString(); const prs = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls?${queryString}`, { repoId }); auditLogger.log({ operation: 'list-pull-requests', operationType: 'READ', componentType: 'PullRequest', success: true, parameters: { repoId, state, base, head, sort, limit, count: prs.length }, executionTimeMs: timer(), }); return prs; } catch (error) { auditLogger.log({ operation: 'list-pull-requests', operationType: 'READ', componentType: 'PullRequest', success: false, error: error.message, parameters: { repoId, state, base, head, sort, limit }, executionTimeMs: timer(), }); throw error; } } /** * Get pull request details */ async getPullRequest(repoId, prNumber) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const pr = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls/${prNumber}`, { repoId }); auditLogger.log({ operation: 'get-pull-request', operationType: 'READ', componentType: 'PullRequest', componentId: prNumber.toString(), success: true, parameters: { repoId, prNumber }, executionTimeMs: timer(), }); return pr; } catch (error) { auditLogger.log({ operation: 'get-pull-request', operationType: 'READ', componentType: 'PullRequest', componentId: prNumber.toString(), success: false, error: error.message, parameters: { repoId, prNumber }, executionTimeMs: timer(), }); throw error; } } /** * Get pull request files */ async getPullRequestFiles(repoId, prNumber) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const files = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls/${prNumber}/files`, { repoId }); auditLogger.log({ operation: 'get-pr-files', operationType: 'READ', componentType: 'PullRequest', componentId: prNumber.toString(), success: true, parameters: { repoId, prNumber, fileCount: files.length }, executionTimeMs: timer(), }); return files; } catch (error) { auditLogger.log({ operation: 'get-pr-files', operationType: 'READ', componentType: 'PullRequest', componentId: prNumber.toString(), success: false, error: error.message, parameters: { repoId, prNumber }, executionTimeMs: timer(), }); throw error; } } /** * Create a new branch (requires GHE_ENABLE_CREATE=true) */ async createBranch(repoId, branchName, fromBranch) { if (!this.config.enableCreate) { throw new Error('Branch creation is disabled. Set GHE_ENABLE_CREATE=true to enable.'); } const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { // Get source branch SHA const sourceBranch = fromBranch || (await this.getDefaultBranch(repoId)).branch; const branchInfo = await this.getBranchDetails(repoId, sourceBranch); const sha = branchInfo.commit.sha; // Create new branch const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/git/refs`, { method: 'POST', data: { ref: `refs/heads/${branchName}`, sha, }, useCache: false, }); auditLogger.log({ operation: 'create-branch', operationType: 'CREATE', componentType: 'Branch', componentName: branchName, success: true, parameters: { repoId, branchName, fromBranch: sourceBranch }, executionTimeMs: timer(), }); return result; } catch (error) { auditLogger.log({ operation: 'create-branch', operationType: 'CREATE', componentType: 'Branch', componentName: branchName, success: false, error: error.message, parameters: { repoId, branchName, fromBranch }, executionTimeMs: timer(), }); throw error; } } /** * Update file content (requires GHE_ENABLE_WRITE=true) */ async updateFile(repoId, path, content, message, branch, sha) { if (!this.config.enableWrite) { throw new Error('File updates are disabled. Set GHE_ENABLE_WRITE=true to enable.'); } const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const encodedContent = Buffer.from(content).toString('base64'); const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}`, { method: 'PUT', data: { message, content: encodedContent, sha, branch, }, useCache: false, }); auditLogger.log({ operation: 'update-file', operationType: 'UPDATE', componentType: 'File', componentName: path, success: true, parameters: { repoId, path, branch, message }, executionTimeMs: timer(), }); return result; } catch (error) { auditLogger.log({ operation: 'update-file', operationType: 'UPDATE', componentType: 'File', componentName: path, success: false, error: error.message, parameters: { repoId, path, branch }, executionTimeMs: timer(), }); throw error; } } /** * Create a new file (requires GHE_ENABLE_CREATE=true) */ async createFile(repoId, path, content, message, branch) { if (!this.config.enableCreate) { throw new Error('File creation is disabled. Set GHE_ENABLE_CREATE=true to enable.'); } const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const encodedContent = Buffer.from(content).toString('base64'); const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}`, { method: 'PUT', data: { message, content: encodedContent, branch, }, useCache: false, }); auditLogger.log({ operation: 'create-file', operationType: 'CREATE', componentType: 'File', componentName: path, success: true, parameters: { repoId, path, branch, message }, executionTimeMs: timer(), }); return result; } catch (error) { auditLogger.log({ operation: 'create-file', operationType: 'CREATE', componentType: 'File', componentName: path, success: false, error: error.message, parameters: { repoId, path, branch }, executionTimeMs: timer(), }); throw error; } } /** * Search repositories */ async searchRepositories(query, owner) { const timer = auditLogger.startTimer(); try { let searchQuery = query; if (owner) { searchQuery += ` org:${owner}`; } const result = await this.makeRequest(`search/repositories?q=${encodeURIComponent(searchQuery)}`, { useCache: false }); auditLogger.log({ operation: 'search-repositories', operationType: 'READ', componentType: 'Repository', success: true, parameters: { query, owner, totalResults: result.total_count }, executionTimeMs: timer(), }); return result; } catch (error) { auditLogger.log({ operation: 'search-repositories', operationType: 'READ', componentType: 'Repository', success: false, error: error.message, parameters: { query, owner }, executionTimeMs: timer(), }); throw error; } } /** * Get directory structure recursively */ async getDirectoryStructure(repoId, path, branch, depth = 3) { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { // Auto-detect branch if not specified const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch; // Recursive function to build tree const buildTree = async (currentPath, currentDepth) => { if (currentDepth > depth) { return { truncated: true }; } const contents = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${currentPath}?ref=${selectedBranch}`, { repoId }); const tree = []; for (const item of contents) { if (item.type === 'dir' && currentDepth < depth) { tree.push({ ...item, children: await buildTree(item.path, currentDepth + 1) }); } else { tree.push(item); } } return tree; }; const tree = await buildTree(path || '', 1); auditLogger.log({ operation: 'get-directory-structure', operationType: 'READ', componentType: 'Directory', componentName: path || '/', success: true, parameters: { repoId, path, branch: selectedBranch, depth }, executionTimeMs: timer(), }); return { tree, branch: selectedBranch }; } catch (error) { auditLogger.log({ operation: 'get-directory-structure', operationType: 'READ', componentType: 'Directory', componentName: path || '/', success: false, error: error.message, parameters: { repoId, path, branch, depth }, executionTimeMs: timer(), }); throw error; } } /** * Get file commit history */ async getFileHistory(repoId, path, branch, limit = 50) { const timer = auditLogger.startTimer(); try { const commits = await this.getCommits(repoId, branch, undefined, undefined, undefined, path, limit); auditLogger.log({ operation: 'get-file-history', operationType: 'READ', componentType: 'File', componentName: path, success: true, parameters: { repoId, path, branch, limit, count: commits.length }, executionTimeMs: timer(), }); return commits; } catch (error) { auditLogger.log({ operation: 'get-file-history', operationType: 'READ', componentType: 'File', componentName: path, success: false, error: error.message, parameters: { repoId, path, branch, limit }, executionTimeMs: timer(), }); throw error; } } /** * Get commit diff */ async getCommitDiff(repoId, sha, format = 'diff') { const timer = auditLogger.startTimer(); const repo = this.getRepoById(repoId); try { const acceptHeader = format === 'patch' ? 'application/vnd.github.v3.patch' : 'application/vnd.github.v3.diff'; const token = await this.getAccessToken(); const url = `${this.baseApiUrl}/repos/${repo.owner}/${repo.repo}/commits/${sha}`; const response = await axios({ method: 'GET', url, headers: { 'Authorization': `token ${token}`, 'Accept': acceptHeader, 'X-GitHub-Api-Version': this.config.apiVersion, }, }); auditLogger.log({ operation: 'get-commit-diff', operationType: 'READ', componentType: 'Commit', componentId: sha, success: true, parameters: { repoId, sha, format }, executionTimeMs: timer(), }); return response.data; } catch (error) { auditLogger.log({ operation: 'get-commit-diff', operationType: 'READ', componentType: 'Commit', componentId: sha, success: false, error: error.message, parameters: { repoId, sha, format }, executionTimeMs: timer(), }); throw error; } } } //# sourceMappingURL=GitHubEnterpriseService.js.map