UNPKG

@tiberriver256/mcp-server-azure-devops

Version:

Azure DevOps reference server for the Model Context Protocol (MCP)

303 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAllRepositoriesTree = getAllRepositoriesTree; exports.formatRepositoryTree = formatRepositoryTree; const GitInterfaces_1 = require("azure-devops-node-api/interfaces/GitInterfaces"); const minimatch_1 = require("minimatch"); const errors_1 = require("../../../shared/errors"); /** * Get tree view of files/directories across multiple repositories * * @param connection The Azure DevOps WebApi connection * @param options Options for getting repository tree * @returns Tree structure for each repository */ async function getAllRepositoriesTree(connection, options) { try { const gitApi = await connection.getGitApi(); let repositories = []; // Get all repositories in the project repositories = await gitApi.getRepositories(options.projectId); // Filter repositories by name pattern if specified if (options.repositoryPattern) { repositories = repositories.filter((repo) => (0, minimatch_1.minimatch)(repo.name || '', options.repositoryPattern || '*')); } // Initialize results array const results = []; // Process each repository for (const repo of repositories) { try { // Get default branch ref const defaultBranch = repo.defaultBranch; if (!defaultBranch) { // Skip repositories with no default branch results.push({ name: repo.name || 'Unknown', tree: [], stats: { directories: 0, files: 0 }, error: 'No default branch found', }); continue; } // Clean the branch name (remove refs/heads/ prefix) const branchRef = defaultBranch.replace('refs/heads/', ''); // Initialize tree items array and counters const treeItems = []; const stats = { directories: 0, files: 0 }; // Determine the recursion level and processing approach const depth = options.depth !== undefined ? options.depth : 0; // Default to 0 (max depth) if (depth === 0) { // For max depth (0), use server-side recursion for better performance const allItems = await gitApi.getItems(repo.id || '', options.projectId, '/', GitInterfaces_1.VersionControlRecursionType.Full, // Use full recursion true, false, false, false, { version: branchRef, versionType: GitInterfaces_1.GitVersionType.Branch, }); // Filter out the root item itself and bad items const itemsToProcess = allItems.filter((item) => item.path !== '/' && item.gitObjectType !== GitInterfaces_1.GitObjectType.Bad); // Process all items at once (they're already retrieved recursively) processItemsNonRecursive(itemsToProcess, treeItems, stats, options.pattern); } else { // For limited depth, use the regular recursive approach // Get items at the root level const rootItems = await gitApi.getItems(repo.id || '', options.projectId, '/', GitInterfaces_1.VersionControlRecursionType.OneLevel, true, false, false, false, { version: branchRef, versionType: GitInterfaces_1.GitVersionType.Branch, }); // Filter out the root item itself and bad items const itemsToProcess = rootItems.filter((item) => item.path !== '/' && item.gitObjectType !== GitInterfaces_1.GitObjectType.Bad); // Process the root items and their children (up to specified depth) await processItems(gitApi, repo.id || '', options.projectId, itemsToProcess, branchRef, treeItems, stats, 1, depth, options.pattern); } // Add repository tree to results results.push({ name: repo.name || 'Unknown', tree: treeItems, stats, }); } catch (repoError) { // Handle errors for individual repositories results.push({ name: repo.name || 'Unknown', tree: [], stats: { directories: 0, files: 0 }, error: `Error processing repository: ${repoError instanceof Error ? repoError.message : String(repoError)}`, }); } } return { repositories: results }; } catch (error) { if (error instanceof errors_1.AzureDevOpsError) { throw error; } throw new Error(`Failed to get repository tree: ${error instanceof Error ? error.message : String(error)}`); } } /** * Process items non-recursively when they're already retrieved with VersionControlRecursionType.Full */ function processItemsNonRecursive(items, result, stats, pattern) { // Sort items (folders first, then by path) const sortedItems = [...items].sort((a, b) => { if (a.isFolder === b.isFolder) { return (a.path || '').localeCompare(b.path || ''); } return a.isFolder ? -1 : 1; }); for (const item of sortedItems) { const name = item.path?.split('/').pop() || ''; const path = item.path || ''; const isFolder = !!item.isFolder; // Skip the root folder if (path === '/') { continue; } // Calculate level from path segments // Remove leading '/' then count segments // For paths like: // /README.md -> ["README.md"] -> length 1 -> level 1 // /src/index.ts -> ["src", "index.ts"] -> length 2 -> level 2 // /src/utils/helper.ts -> ["src", "utils", "helper.ts"] -> length 3 -> level 3 const pathSegments = path.replace(/^\//, '').split('/'); const level = pathSegments.length; // Filter files based on pattern (if specified) if (!isFolder && pattern && !(0, minimatch_1.minimatch)(name, pattern)) { continue; } // Add item to results result.push({ name, path, isFolder, level, }); // Update counters if (isFolder) { stats.directories++; } else { stats.files++; } } } /** * Process items recursively up to the specified depth */ async function processItems(gitApi, repoId, projectId, items, branchRef, result, stats, currentDepth, maxDepth, pattern) { // Sort items (directories first, then files) const sortedItems = [...items].sort((a, b) => { if (a.isFolder === b.isFolder) { return (a.path || '').localeCompare(b.path || ''); } return a.isFolder ? -1 : 1; }); for (const item of sortedItems) { const name = item.path?.split('/').pop() || ''; const path = item.path || ''; const isFolder = !!item.isFolder; // Filter files based on pattern (if specified) if (!isFolder && pattern && !(0, minimatch_1.minimatch)(name, pattern)) { continue; } // Add item to results result.push({ name, path, isFolder, level: currentDepth, }); // Update counters if (isFolder) { stats.directories++; } else { stats.files++; } // Recursively process folders if not yet at max depth if (isFolder && currentDepth < maxDepth) { try { const childItems = await gitApi.getItems(repoId, projectId, path, GitInterfaces_1.VersionControlRecursionType.OneLevel, true, false, false, false, { version: branchRef, versionType: GitInterfaces_1.GitVersionType.Branch, }); // Filter out the parent folder itself and bad items const itemsToProcess = childItems.filter((child) => child.path !== path && child.gitObjectType !== GitInterfaces_1.GitObjectType.Bad); // Process child items await processItems(gitApi, repoId, projectId, itemsToProcess, branchRef, result, stats, currentDepth + 1, maxDepth, pattern); } catch (error) { // Ignore errors in child items and continue with siblings console.error(`Error processing folder ${path}: ${error}`); } } } } /** * Convert the tree items to a formatted ASCII string representation * * @param repoName Repository name * @param items Tree items * @param stats Statistics about files and directories * @returns Formatted ASCII string */ function formatRepositoryTree(repoName, items, stats, error) { let output = `${repoName}/\n`; if (error) { output += ` (${error})\n`; } else if (items.length === 0) { output += ' (Repository is empty or default branch not found)\n'; } else { // Sort items by path to ensure proper sequence const sortedItems = [...items].sort((a, b) => { // Sort by level first if (a.level !== b.level) { return a.level - b.level; } // Then folders before files if (a.isFolder !== b.isFolder) { return a.isFolder ? -1 : 1; } // Then alphabetically return a.path.localeCompare(b.path); }); // Create a structured tree representation const tree = createTreeStructure(sortedItems); // Format the tree starting from the root output += formatTree(tree, ' '); } // Add summary line output += `${stats.directories} directories, ${stats.files} files\n`; return output; } /** * Create a structured tree from the flat list of items */ function createTreeStructure(items) { const root = { name: '', path: '', isFolder: true, children: [], }; // Map to track all nodes by path const nodeMap = { '': root }; // First create all nodes for (const item of items) { nodeMap[item.path] = { name: item.name, path: item.path, isFolder: item.isFolder, children: [], }; } // Then build the hierarchy for (const item of items) { if (item.path === '/') continue; const node = nodeMap[item.path]; const lastSlashIndex = item.path.lastIndexOf('/'); // For root level items, the parent path is empty const parentPath = lastSlashIndex <= 0 ? '' : item.path.substring(0, lastSlashIndex); // Get parent node (defaults to root if parent not found) const parent = nodeMap[parentPath] || root; // Add this node as a child of its parent parent.children.push(node); } return root; } /** * Format a tree structure into an ASCII tree representation */ function formatTree(node, indent) { if (!node.children.length) return ''; let output = ''; // Sort the children: folders first, then alphabetically const children = [...node.children].sort((a, b) => { if (a.isFolder !== b.isFolder) { return a.isFolder ? -1 : 1; } return a.name.localeCompare(b.name); }); // Format each child node for (let i = 0; i < children.length; i++) { const child = children[i]; const isLast = i === children.length - 1; const connector = isLast ? '`-- ' : '|-- '; const childIndent = isLast ? ' ' : '| '; // Add the node itself const suffix = child.isFolder ? '/' : ''; output += `${indent}${connector}${child.name}${suffix}\n`; // Recursively add its children if (child.children.length > 0) { output += formatTree(child, indent + childIndent); } } return output; } //# sourceMappingURL=feature.js.map