Skip to main content
Glama
MUSE-CODE-SPACE

Vibe Coding Documentation MCP (MUSE)

gitParsers.ts9.82 kB
/** * Git output parsing utilities */ // File status types export type FileStatusType = | 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'untracked' | 'ignored' | 'unmerged'; export interface GitFileStatus { path: string; status: FileStatusType; staged: boolean; oldPath?: string; } export interface ParsedStatus { branch: string; upstream?: string; ahead: number; behind: number; staged: GitFileStatus[]; unstaged: GitFileStatus[]; untracked: GitFileStatus[]; conflicts: GitFileStatus[]; } export interface GitCommit { hash: string; shortHash: string; author: string; authorEmail: string; date: string; message: string; body?: string; files?: string[]; } export interface DiffFileStat { path: string; additions: number; deletions: number; binary: boolean; } export interface ParsedDiff { files: DiffFileStat[]; totalAdditions: number; totalDeletions: number; } export interface BranchInfo { name: string; current: boolean; upstream?: string; ahead?: number; behind?: number; lastCommit?: string; lastCommitDate?: string; } /** * Parse git status --porcelain=v2 --branch output */ export function parseStatusPorcelainV2(output: string): ParsedStatus { const lines = output.split('\n').filter((line) => line.length > 0); let branch = 'HEAD'; let upstream: string | undefined; let ahead = 0; let behind = 0; const staged: GitFileStatus[] = []; const unstaged: GitFileStatus[] = []; const untracked: GitFileStatus[] = []; const conflicts: GitFileStatus[] = []; for (const line of lines) { // Branch header lines if (line.startsWith('# branch.oid')) { continue; } if (line.startsWith('# branch.head')) { branch = line.split(' ')[2] || 'HEAD'; continue; } if (line.startsWith('# branch.upstream')) { upstream = line.split(' ')[2]; continue; } if (line.startsWith('# branch.ab')) { const match = line.match(/\+(\d+) -(\d+)/); if (match) { ahead = parseInt(match[1], 10); behind = parseInt(match[2], 10); } continue; } // Untracked files if (line.startsWith('?')) { const path = line.slice(2); untracked.push({ path, status: 'untracked', staged: false }); continue; } // Ignored files if (line.startsWith('!')) { continue; } // Changed entries (1 = ordinary, 2 = renamed/copied) if (line.startsWith('1') || line.startsWith('2')) { const parts = line.split('\t'); const info = parts[0].split(' '); const xy = info[1]; // XY status codes const path = parts[parts.length - 1]; const oldPath = parts.length > 2 ? parts[1] : undefined; const indexStatus = xy[0]; const worktreeStatus = xy[1]; // Staged changes if (indexStatus !== '.') { staged.push({ path, status: mapStatusCode(indexStatus), staged: true, oldPath, }); } // Unstaged changes if (worktreeStatus !== '.') { unstaged.push({ path, status: mapStatusCode(worktreeStatus), staged: false, }); } continue; } // Unmerged entries if (line.startsWith('u')) { const parts = line.split('\t'); const path = parts[parts.length - 1]; conflicts.push({ path, status: 'unmerged', staged: false }); } } return { branch, upstream, ahead, behind, staged, unstaged, untracked, conflicts }; } /** * Map git status code to FileStatusType */ function mapStatusCode(code: string): FileStatusType { switch (code) { case 'M': return 'modified'; case 'A': return 'added'; case 'D': return 'deleted'; case 'R': return 'renamed'; case 'C': return 'copied'; case 'U': return 'unmerged'; default: return 'modified'; } } /** * Parse git log with custom format * Format: %H|%h|%an|%ae|%aI|%s */ export function parseLogOutput(output: string, includeBody: boolean = false): GitCommit[] { if (!output.trim()) { return []; } const commits: GitCommit[] = []; const separator = includeBody ? 'END_COMMIT\n' : '\n'; const entries = output.split(separator).filter((e) => e.trim()); for (const entry of entries) { const lines = entry.trim().split('\n'); if (lines.length === 0) continue; const firstLine = lines[0]; const parts = firstLine.split('|'); if (parts.length >= 6) { const commit: GitCommit = { hash: parts[0], shortHash: parts[1], author: parts[2], authorEmail: parts[3], date: parts[4], message: parts.slice(5).join('|'), // Handle | in message }; if (includeBody && lines.length > 1) { commit.body = lines.slice(1).join('\n').trim(); } commits.push(commit); } } return commits; } /** * Parse git diff --stat output */ export function parseDiffStat(output: string): ParsedDiff { const lines = output.split('\n'); const files: DiffFileStat[] = []; let totalAdditions = 0; let totalDeletions = 0; for (const line of lines) { // Match file stat lines: " path | 10 ++---" const match = line.match(/^\s*(.+?)\s*\|\s*(\d+|Bin)/); if (match) { const path = match[1].trim(); const isBinary = match[2] === 'Bin'; let additions = 0; let deletions = 0; if (!isBinary) { const plusCount = (line.match(/\+/g) || []).length; const minusCount = (line.match(/-/g) || []).length; additions = plusCount; deletions = minusCount; } files.push({ path, additions, deletions, binary: isBinary }); totalAdditions += additions; totalDeletions += deletions; } // Match summary line: " 3 files changed, 10 insertions(+), 5 deletions(-)" const summaryMatch = line.match( /(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/ ); if (summaryMatch) { totalAdditions = parseInt(summaryMatch[2] || '0', 10); totalDeletions = parseInt(summaryMatch[3] || '0', 10); } } return { files, totalAdditions, totalDeletions }; } /** * Parse git branch output * Format: %(refname:short)|%(objectname:short)|%(upstream:short)|%(upstream:track,nobracket) */ export function parseBranchOutput(output: string, currentBranch: string): BranchInfo[] { const lines = output.split('\n').filter((l) => l.trim()); const branches: BranchInfo[] = []; for (const line of lines) { const parts = line.split('|'); if (parts.length < 2) continue; const name = parts[0].trim(); const lastCommit = parts[1]?.trim(); const upstream = parts[2]?.trim() || undefined; const trackInfo = parts[3]?.trim(); let ahead: number | undefined; let behind: number | undefined; if (trackInfo) { const aheadMatch = trackInfo.match(/ahead (\d+)/); const behindMatch = trackInfo.match(/behind (\d+)/); if (aheadMatch) ahead = parseInt(aheadMatch[1], 10); if (behindMatch) behind = parseInt(behindMatch[1], 10); } branches.push({ name, current: name === currentBranch, upstream, ahead, behind, lastCommit, }); } return branches; } /** * Parse git stash list output * Format: stash@{0}|message|date */ export function parseStashList(output: string): Array<{ index: number; message: string; date: string }> { if (!output.trim()) { return []; } const lines = output.split('\n').filter((l) => l.trim()); const stashes: Array<{ index: number; message: string; date: string }> = []; for (const line of lines) { const parts = line.split('|'); if (parts.length >= 3) { const indexMatch = parts[0].match(/stash@\{(\d+)\}/); if (indexMatch) { stashes.push({ index: parseInt(indexMatch[1], 10), message: parts[1], date: parts[2], }); } } } return stashes; } /** * Detect language from text (simple heuristic) */ export function detectLanguage(text: string): 'en' | 'ko' { const koreanPattern = /[가-힣]/; return koreanPattern.test(text) ? 'ko' : 'en'; } /** * Infer design decision category from text */ export function inferCategory( text: string, lang: 'en' | 'ko' ): 'architecture' | 'library' | 'pattern' | 'implementation' | 'other' { const patterns = { architecture: { en: /architecture|system design|infrastructure|microservice|monolith/i, ko: /아키텍처|시스템 설계|인프라|마이크로서비스/, }, library: { en: /library|package|dependency|npm|framework|sdk/i, ko: /라이브러리|패키지|의존성|프레임워크/, }, pattern: { en: /pattern|design pattern|singleton|factory|observer|mvc|mvvm/i, ko: /패턴|디자인 패턴|싱글톤|팩토리/, }, implementation: { en: /implement|feature|refactor|fix|optimize|improve/i, ko: /구현|기능|리팩토링|수정|최적화|개선/, }, }; for (const [category, langPatterns] of Object.entries(patterns)) { if (langPatterns[lang].test(text)) { return category as 'architecture' | 'library' | 'pattern' | 'implementation'; } } return 'other'; } /** * Calculate commit importance based on content */ export function calculateCommitImportance( commit: GitCommit, fullMessage: string ): 'high' | 'medium' | 'low' { const highPatterns = /breaking|major|critical|important|architecture|migrate|security/i; const mediumPatterns = /refactor|redesign|feature|implement|add|update/i; if (highPatterns.test(fullMessage)) { return 'high'; } if (mediumPatterns.test(fullMessage)) { return 'medium'; } return 'low'; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MUSE-CODE-SPACE/vibe-coding-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server