@mcp-consultant-tools/github-enterprise
Version:
MCP server for GitHub Enterprise - repositories, commits, pull requests, and code search
331 lines • 14.2 kB
JavaScript
/**
* GitHub Enterprise Formatters
* Transform GitHub API responses into human-readable markdown
*/
/**
* Format branch list as markdown table
*/
export function formatBranchListAsMarkdown(branches) {
if (!branches || branches.length === 0) {
return '*No branches found*';
}
const header = '| Branch | Last Commit | Author | Date | Protected |';
const separator = '|--------|-------------|--------|------|-----------|';
const rows = branches.map(b => {
const commitMsg = b.commit?.commit?.message?.split('\n')[0] || 'N/A';
const author = b.commit?.commit?.author?.name || 'N/A';
const date = b.commit?.commit?.author?.date
? new Date(b.commit.commit.author.date).toLocaleDateString()
: 'N/A';
const protected_icon = b.protected ? '🔒' : '';
return `| ${b.name} | ${commitMsg.substring(0, 50)}${commitMsg.length > 50 ? '...' : ''} | ${author} | ${date} | ${protected_icon} |`;
});
return [header, separator, ...rows].join('\n');
}
/**
* Format commit history as markdown
*/
export function formatCommitHistoryAsMarkdown(commits) {
if (!commits || commits.length === 0) {
return '*No commits found*';
}
const sections = commits.map((c, index) => {
const shortSha = c.sha.substring(0, 7);
const message = c.commit.message.split('\n')[0];
const author = c.commit.author.name;
const date = new Date(c.commit.author.date).toLocaleDateString();
return `### ${index + 1}. \`${shortSha}\` - ${message}\n` +
`**Author:** ${author} \n` +
`**Date:** ${date} \n` +
`**Parents:** ${c.parents.map((p) => p.sha.substring(0, 7)).join(', ')}\n`;
});
return sections.join('\n');
}
/**
* Format code search results as markdown with highlighting
*/
export function formatCodeSearchResultsAsMarkdown(results) {
if (!results || !results.items || results.items.length === 0) {
return '*No code matches found*';
}
const sections = results.items.map((item, index) => {
const repo = `${item.repository.owner.login}/${item.repository.name}`;
const score = '⭐'.repeat(Math.min(5, Math.ceil(item.score / 5)));
let matches = '';
if (item.text_matches && item.text_matches.length > 0) {
matches = item.text_matches.map((m) => {
const fragment = m.fragment.substring(0, 200);
return `\`\`\`\n${fragment}${fragment.length >= 200 ? '...' : ''}\n\`\`\``;
}).join('\n\n');
}
return `### ${index + 1}. ${repo}/${item.path}\n` +
`**Relevance:** ${score} \n` +
`**File:** \`${item.name}\` \n` +
`**URL:** ${item.html_url}\n\n` +
(matches ? `**Matches:**\n${matches}\n` : '');
});
return `# Code Search Results\n\n` +
`**Total Matches:** ${results.total_count} \n` +
`**Showing:** ${results.items.length} results\n\n` +
sections.join('\n---\n\n');
}
/**
* Format pull requests as markdown table
*/
export function formatPullRequestsAsMarkdown(prs) {
if (!prs || prs.length === 0) {
return '*No pull requests found*';
}
const header = '| # | Title | State | Author | Created | Updated |';
const separator = '|---|-------|-------|--------|---------|---------|';
const rows = prs.map(pr => {
const state = pr.state === 'open' ? '🟢 Open' : pr.merged_at ? '🟣 Merged' : '🔴 Closed';
const title = pr.title.substring(0, 40) + (pr.title.length > 40 ? '...' : '');
const author = pr.user.login;
const created = new Date(pr.created_at).toLocaleDateString();
const updated = new Date(pr.updated_at).toLocaleDateString();
return `| #${pr.number} | ${title} | ${state} | ${author} | ${created} | ${updated} |`;
});
return [header, separator, ...rows].join('\n');
}
/**
* Format file tree as markdown
*/
export function formatFileTreeAsMarkdown(tree, prefix = '') {
if (!tree || tree.length === 0) {
return '*Empty directory*';
}
const lines = [];
tree.forEach((item, index) => {
const isLast = index === tree.length - 1;
const connector = isLast ? '└── ' : '├── ';
const icon = item.type === 'dir' ? '📁' : item.type === 'file' ? '📄' : '📦';
lines.push(`${prefix}${connector}${icon} ${item.name}`);
if (item.children) {
const childPrefix = prefix + (isLast ? ' ' : '│ ');
lines.push(formatFileTreeAsMarkdown(item.children, childPrefix));
}
});
return lines.join('\n');
}
/**
* Format directory contents as markdown table
*/
export function formatDirectoryContentsAsMarkdown(contents) {
if (!contents || contents.length === 0) {
return '*Empty directory*';
}
const header = '| Type | Name | Size | SHA |';
const separator = '|------|------|------|-----|';
const rows = contents.map(item => {
const type = item.type === 'dir' ? '📁 Directory' : '📄 File';
const size = item.size ? `${(item.size / 1024).toFixed(2)} KB` : 'N/A';
const sha = item.sha.substring(0, 7);
return `| ${type} | ${item.name} | ${size} | \`${sha}\` |`;
});
return [header, separator, ...rows].join('\n');
}
/**
* Analyze branch comparison and extract insights
*/
export function analyzeBranchComparison(comparison) {
const insights = [];
if (!comparison) {
return ['No comparison data available'];
}
// Ahead/behind status
insights.push(`- **${comparison.ahead_by} commits ahead**, **${comparison.behind_by} commits behind** base`);
// File statistics
if (comparison.files && comparison.files.length > 0) {
const totalAdditions = comparison.files.reduce((sum, f) => sum + f.additions, 0);
const totalDeletions = comparison.files.reduce((sum, f) => sum + f.deletions, 0);
insights.push(`- **${comparison.files.length} files changed** (+${totalAdditions}, -${totalDeletions})`);
// Group by file type
const byExtension = {};
comparison.files.forEach((f) => {
const ext = f.filename.split('.').pop() || 'unknown';
byExtension[ext] = (byExtension[ext] || 0) + 1;
});
const topExtensions = Object.entries(byExtension)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([ext, count]) => `${ext} (${count})`)
.join(', ');
insights.push(`- **File types:** ${topExtensions}`);
}
// Commit analysis
if (comparison.commits && comparison.commits.length > 0) {
const authors = new Set(comparison.commits.map((c) => c.commit.author.name));
insights.push(`- **Contributors:** ${authors.size} (${Array.from(authors).join(', ')})`);
// Check for work item references
const workItemRefs = comparison.commits
.map((c) => c.commit.message.match(/#(\d+)|AB#(\d+)/g))
.filter((m) => m)
.flat();
if (workItemRefs.length > 0) {
const uniqueRefs = [...new Set(workItemRefs)];
insights.push(`- **Work items referenced:** ${uniqueRefs.join(', ')}`);
}
}
return insights;
}
/**
* Generate deployment checklist from changes
*/
export function generateDeploymentChecklist(comparison) {
const checklist = [];
if (!comparison || !comparison.files) {
return ['- [ ] Verify changes before deployment'];
}
// Check for specific file patterns
const hasPluginChanges = comparison.files.some((f) => f.filename.includes('Plugin'));
const hasTestChanges = comparison.files.some((f) => f.filename.includes('Test'));
const hasConfigChanges = comparison.files.some((f) => f.filename.includes('config') || f.filename.includes('.json') || f.filename.includes('.xml'));
const hasDbChanges = comparison.files.some((f) => f.filename.includes('.sql') || f.filename.includes('migration'));
checklist.push('### Pre-Deployment');
checklist.push('- [ ] Code review approved');
checklist.push('- [ ] All tests passing');
if (hasPluginChanges) {
checklist.push('- [ ] Build plugin assemblies');
checklist.push('- [ ] Verify plugin registration');
}
if (hasConfigChanges) {
checklist.push('- [ ] Review configuration changes');
checklist.push('- [ ] Update environment variables');
}
if (hasDbChanges) {
checklist.push('- [ ] Test database migrations');
checklist.push('- [ ] Backup production database');
}
checklist.push('');
checklist.push('### Deployment');
if (hasPluginChanges) {
checklist.push('- [ ] Upload plugin assemblies to PowerPlatform');
checklist.push('- [ ] Publish customizations');
}
if (hasDbChanges) {
checklist.push('- [ ] Run database migrations');
}
checklist.push('- [ ] Deploy code to production');
checklist.push('- [ ] Verify deployment success');
checklist.push('');
checklist.push('### Post-Deployment');
checklist.push('- [ ] Smoke tests in production');
checklist.push('- [ ] Monitor error logs (first 1 hour)');
if (hasPluginChanges) {
checklist.push('- [ ] Check plugin trace logs');
}
checklist.push('- [ ] Update documentation');
checklist.push('- [ ] Close related work items');
return checklist;
}
/**
* Format commit details with file changes
*/
export function formatCommitDetailsAsMarkdown(commit) {
if (!commit) {
return '*No commit data*';
}
const shortSha = commit.sha.substring(0, 7);
const message = commit.commit.message;
const author = commit.commit.author.name;
const email = commit.commit.author.email;
const date = new Date(commit.commit.author.date).toLocaleString();
let output = `# Commit \`${shortSha}\`\n\n`;
output += `**Message:**\n\`\`\`\n${message}\n\`\`\`\n\n`;
output += `**Author:** ${author} <${email}> \n`;
output += `**Date:** ${date} \n`;
output += `**Parents:** ${commit.parents.map((p) => p.sha.substring(0, 7)).join(', ')}\n\n`;
if (commit.stats) {
output += `**Stats:** +${commit.stats.additions} / -${commit.stats.deletions} (${commit.stats.total} changes)\n\n`;
}
if (commit.files && commit.files.length > 0) {
output += `## Files Changed (${commit.files.length})\n\n`;
const header = '| File | Status | +/- | Changes |';
const separator = '|------|--------|-----|---------|';
const rows = commit.files.map((f) => {
const status = f.status === 'added' ? '🆕 Added' :
f.status === 'modified' ? '📝 Modified' :
f.status === 'removed' ? '🗑️ Removed' :
f.status === 'renamed' ? '📋 Renamed' : f.status;
return `| \`${f.filename}\` | ${status} | +${f.additions}/-${f.deletions} | ${f.changes} |`;
});
output += [header, separator, ...rows].join('\n');
}
return output;
}
/**
* Format pull request details
*/
export function formatPullRequestDetailsAsMarkdown(pr) {
if (!pr) {
return '*No pull request data*';
}
const state = pr.state === 'open' ? '🟢 Open' : pr.merged_at ? '🟣 Merged' : '🔴 Closed';
const mergeable = pr.mergeable === true ? '✅ Yes' :
pr.mergeable === false ? '❌ No (conflicts)' : '⏳ Checking...';
let output = `# Pull Request #${pr.number}: ${pr.title}\n\n`;
output += `**State:** ${state} \n`;
output += `**Author:** ${pr.user.login} \n`;
output += `**Created:** ${new Date(pr.created_at).toLocaleString()} \n`;
output += `**Updated:** ${new Date(pr.updated_at).toLocaleString()} \n`;
if (pr.merged_at) {
output += `**Merged:** ${new Date(pr.merged_at).toLocaleString()} \n`;
output += `**Merged by:** ${pr.merged_by?.login || 'N/A'} \n`;
}
output += `**Mergeable:** ${mergeable} \n`;
output += `**Base:** \`${pr.base.ref}\` ← **Head:** \`${pr.head.ref}\` \n\n`;
if (pr.body) {
output += `## Description\n\n${pr.body}\n\n`;
}
output += `## Stats\n\n`;
output += `- **Commits:** ${pr.commits} \n`;
output += `- **Files Changed:** ${pr.changed_files} \n`;
output += `- **Additions:** +${pr.additions} \n`;
output += `- **Deletions:** -${pr.deletions} \n`;
output += `- **Comments:** ${pr.comments} \n`;
output += `- **Review Comments:** ${pr.review_comments} \n\n`;
output += `**URL:** ${pr.html_url}\n`;
return output;
}
/**
* Format repository overview
*/
export function formatRepositoryOverviewAsMarkdown(repo, branches, recentCommits) {
let output = `# Repository Overview: ${repo.owner}/${repo.repo}\n\n`;
output += `**URL:** ${repo.url} \n`;
if (repo.defaultBranch) {
output += `**Default Branch:** \`${repo.defaultBranch}\` \n`;
}
if (repo.description) {
output += `**Description:** ${repo.description} \n`;
}
output += `**Status:** ${repo.active ? '🟢 Active' : '🔴 Inactive'}\n\n`;
if (branches && branches.length > 0) {
output += `## Branches (${branches.length})\n\n`;
output += formatBranchListAsMarkdown(branches.slice(0, 10));
if (branches.length > 10) {
output += `\n\n*Showing 10 of ${branches.length} branches*`;
}
output += '\n\n';
}
if (recentCommits && recentCommits.length > 0) {
output += `## Recent Activity (Last ${recentCommits.length} commits)\n\n`;
output += formatCommitHistoryAsMarkdown(recentCommits.slice(0, 5));
output += '\n\n';
}
return output;
}
/**
* Sanitize error messages (remove sensitive data)
*/
export function sanitizeErrorMessage(error) {
let message = typeof error === 'string' ? error : error.message || 'Unknown error';
// Remove tokens
message = message.replace(/ghp_[a-zA-Z0-9]{36}/g, 'ghp_***');
message = message.replace(/ghs_[a-zA-Z0-9]{36}/g, 'ghs_***');
// Remove URLs with tokens
message = message.replace(/https:\/\/.*:.*@/g, 'https://***:***@');
return message;
}
//# sourceMappingURL=ghe-formatters.js.map