Skip to main content
Glama
diff.ts5.2 kB
/** * @fileoverview CLI provider git diff operation * @module services/git/providers/cli/operations/commits/diff */ import type { RequestContext } from '@/utils/index.js'; import type { GitDiffOptions, GitDiffResult, GitOperationContext, } from '../../../../types.js'; import { buildGitCommand, mapGitError, parseGitDiffStat, } from '../../utils/index.js'; /** * Execute git diff to show changes. * * @param options - Diff options * @param context - Operation context * @param execGit - Function to execute git commands * @returns Diff result */ export async function executeDiff( options: GitDiffOptions, context: GitOperationContext, execGit: ( args: string[], cwd: string, ctx: RequestContext, ) => Promise<{ stdout: string; stderr: string }>, ): Promise<GitDiffResult> { try { const args: string[] = []; // Add flags first (before commits/paths) if (options.staged) { args.push('--cached'); } if (options.nameOnly) { args.push('--name-only'); } if (options.unified !== undefined) { args.push(`--unified=${options.unified}`); } // Add commit refs if (options.commit1) { args.push(options.commit1); } if (options.commit2) { args.push(options.commit2); } // Add path filter last (after --) if (options.path) { args.push('--', options.path); } // If stat-only mode requested, just return stat output if (options.stat) { const statCmd = buildGitCommand({ command: 'diff', args: [...args.filter((a) => a !== '--name-only'), '--stat'], }); const statResult = await execGit( statCmd, context.workingDirectory, context.requestContext, ); const stats = parseGitDiffStat(statResult.stdout); return { diff: statResult.stdout, filesChanged: stats.files.length, insertions: stats.totalAdditions, deletions: stats.totalDeletions, binary: statResult.stdout.includes('Binary files'), }; } // Get diff content const diffCmd = buildGitCommand({ command: 'diff', args }); const diffResult = await execGit( diffCmd, context.workingDirectory, context.requestContext, ); // If includeUntracked, get untracked files and append their diff let untrackedDiff = ''; let untrackedFileCount = 0; if (options.includeUntracked) { // Get list of untracked files const lsFilesCmd = buildGitCommand({ command: 'ls-files', args: ['--others', '--exclude-standard'], }); const lsFilesResult = await execGit( lsFilesCmd, context.workingDirectory, context.requestContext, ); const untrackedFiles = lsFilesResult.stdout .split('\n') .filter((f) => f.trim()); untrackedFileCount = untrackedFiles.length; // Generate diff for each untracked file (show as new file) for (const file of untrackedFiles) { if (options.nameOnly) { untrackedDiff += `${file}\n`; } else { // Use git diff --no-index to show untracked file as new // Note: git diff --no-index exits with 1 when files differ, which is expected try { const untrackedCmd = buildGitCommand({ command: 'diff', args: ['--no-index', '/dev/null', file], }); const result = await execGit( untrackedCmd, context.workingDirectory, context.requestContext, ); untrackedDiff += result.stdout; } catch (err: unknown) { // git diff --no-index exits with code 1 when files differ // The error message format is: "Exit Code: N\nStderr: ...\nStdout: ..." if (err instanceof Error) { const stdoutMatch = err.message.match(/\nStdout: ([\s\S]*)$/); if (stdoutMatch?.[1]) { untrackedDiff += stdoutMatch[1]; } } } } } } // Combine tracked and untracked diffs const combinedDiff = diffResult.stdout + untrackedDiff; // For name-only mode, count files from output if (options.nameOnly) { const files = combinedDiff.split('\n').filter((line) => line.trim()); return { diff: combinedDiff, filesChanged: files.length, binary: false, }; } // Get diff stats for full diff mode const baseArgs = args.filter((a) => a !== '--name-only'); const statCmd = buildGitCommand({ command: 'diff', args: [...baseArgs, '--stat'], }); const statResult = await execGit( statCmd, context.workingDirectory, context.requestContext, ); const stats = parseGitDiffStat(statResult.stdout); const hasBinary = combinedDiff.includes('Binary files'); return { diff: combinedDiff, filesChanged: stats.files.length + untrackedFileCount, insertions: stats.totalAdditions, deletions: stats.totalDeletions, binary: hasBinary, }; } catch (error) { throw mapGitError(error, 'diff'); } }

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/cyanheads/git-mcp-server'

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