Skip to main content
Glama

Git MCP Server

by Sheshiyer
sync-operations.ts10.8 kB
import { BaseGitOperation } from '../base/base-operation.js'; import { GitCommandBuilder } from '../../common/command-builder.js'; import { CommandResult } from '../base/operation-result.js'; import { ErrorHandler } from '../../errors/error-handler.js'; import { RepositoryValidator } from '../../utils/repository.js'; import { CommandExecutor } from '../../utils/command.js'; import { RepoStateType } from '../../caching/repository-cache.js'; import { PushOptions, PullOptions, FetchOptions, PushResult, PullResult, FetchResult } from './sync-types.js'; /** * Handles Git push operations */ export class PushOperation extends BaseGitOperation<PushOptions, PushResult> { protected buildCommand(): GitCommandBuilder { const command = GitCommandBuilder.push(); if (this.options.remote) { command.arg(this.options.remote); } if (this.options.branch) { command.arg(this.options.branch); } if (this.options.force) { command.withForce(); } if (this.options.forceWithLease) { command.flag('force-with-lease'); } if (this.options.all) { command.flag('all'); } if (this.options.tags) { command.flag('tags'); } if (this.options.noVerify) { command.withNoVerify(); } if (this.options.setUpstream) { command.withSetUpstream(); } if (this.options.prune) { command.flag('prune'); } return command; } protected parseResult(result: CommandResult): PushResult { const summary = { created: [] as string[], deleted: [] as string[], updated: [] as string[], rejected: [] as string[] }; // Parse push output result.stdout.split('\n').forEach(line => { if (line.startsWith('To ')) return; // Skip remote URL line const match = line.match(/^\s*([a-f0-9]+)\.\.([a-f0-9]+)\s+(\S+)\s+->\s+(\S+)/); if (match) { const [, oldRef, newRef, localRef, remoteRef] = match; summary.updated.push(remoteRef); } else if (line.includes('[new branch]')) { const branchMatch = line.match(/\[new branch\]\s+(\S+)\s+->\s+(\S+)/); if (branchMatch) { summary.created.push(branchMatch[2]); } } else if (line.includes('[deleted]')) { const deleteMatch = line.match(/\[deleted\]\s+(\S+)/); if (deleteMatch) { summary.deleted.push(deleteMatch[1]); } } else if (line.includes('! [rejected]')) { const rejectMatch = line.match(/\! \[rejected\]\s+(\S+)/); if (rejectMatch) { summary.rejected.push(rejectMatch[1]); } } }); return { remote: this.options.remote || 'origin', branch: this.options.branch, forced: this.options.force || false, summary: { created: summary.created.length > 0 ? summary.created : undefined, deleted: summary.deleted.length > 0 ? summary.deleted : undefined, updated: summary.updated.length > 0 ? summary.updated : undefined, rejected: summary.rejected.length > 0 ? summary.rejected : undefined }, raw: result.stdout }; } protected getCacheConfig() { return { command: 'push', stateType: RepoStateType.REMOTE }; } protected async validateOptions(): Promise<void> { if (!this.options.branch && !this.options.all) { throw ErrorHandler.handleValidationError( new Error('Either branch or --all must be specified'), { operation: this.context.operation } ); } if (this.options.remote) { await RepositoryValidator.validateRemoteConfig( this.getResolvedPath(), this.options.remote, this.context.operation ); } if (this.options.branch) { await RepositoryValidator.validateBranchExists( this.getResolvedPath(), this.options.branch, this.context.operation ); } } } /** * Handles Git pull operations */ export class PullOperation extends BaseGitOperation<PullOptions, PullResult> { protected buildCommand(): GitCommandBuilder { const command = GitCommandBuilder.pull(); if (this.options.remote) { command.arg(this.options.remote); } if (this.options.branch) { command.arg(this.options.branch); } if (this.options.rebase) { command.flag('rebase'); } if (this.options.autoStash) { command.flag('autostash'); } if (this.options.allowUnrelated) { command.flag('allow-unrelated-histories'); } if (this.options.ff === 'only') { command.flag('ff-only'); } else if (this.options.ff === 'no') { command.flag('no-ff'); } if (this.options.strategy) { command.option('strategy', this.options.strategy); } if (this.options.strategyOption) { this.options.strategyOption.forEach(opt => { command.option('strategy-option', opt); }); } return command; } protected parseResult(result: CommandResult): PullResult { const summary = { merged: [] as string[], conflicts: [] as string[] }; let filesChanged = 0; let insertions = 0; let deletions = 0; // Parse pull output result.stdout.split('\n').forEach(line => { if (line.includes('|')) { // Parse merge stats const statsMatch = line.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/); if (statsMatch) { filesChanged = parseInt(statsMatch[1], 10); insertions = statsMatch[2] ? parseInt(statsMatch[2], 10) : 0; deletions = statsMatch[3] ? parseInt(statsMatch[3], 10) : 0; } } else if (line.includes('Fast-forward') || line.includes('Merge made by')) { // Track merged files const mergeMatch = line.match(/([^/]+)$/); if (mergeMatch) { summary.merged.push(mergeMatch[1]); } } else if (line.includes('CONFLICT')) { // Track conflicts const conflictMatch = line.match(/CONFLICT \(.+?\): (.+)/); if (conflictMatch) { summary.conflicts.push(conflictMatch[1]); } } }); return { remote: this.options.remote || 'origin', branch: this.options.branch, rebased: this.options.rebase || false, filesChanged, insertions, deletions, summary: { merged: summary.merged.length > 0 ? summary.merged : undefined, conflicts: summary.conflicts.length > 0 ? summary.conflicts : undefined }, raw: result.stdout }; } protected getCacheConfig() { return { command: 'pull', stateType: RepoStateType.REMOTE }; } protected async validateOptions(): Promise<void> { if (!this.options.branch) { throw ErrorHandler.handleValidationError( new Error('Branch must be specified'), { operation: this.context.operation } ); } if (this.options.remote) { await RepositoryValidator.validateRemoteConfig( this.getResolvedPath(), this.options.remote, this.context.operation ); } // Ensure working tree is clean unless autostash is enabled if (!this.options.autoStash) { await RepositoryValidator.ensureClean( this.getResolvedPath(), this.context.operation ); } } } /** * Handles Git fetch operations */ export class FetchOperation extends BaseGitOperation<FetchOptions, FetchResult> { protected buildCommand(): GitCommandBuilder { const command = GitCommandBuilder.fetch(); if (this.options.remote && !this.options.all) { command.arg(this.options.remote); } if (this.options.all) { command.flag('all'); } if (this.options.prune) { command.flag('prune'); } if (this.options.pruneTags) { command.flag('prune-tags'); } if (this.options.tags) { command.flag('tags'); } if (this.options.tagsOnly) { command.flag('tags').flag('no-recurse-submodules'); } if (this.options.forceTags) { command.flag('force').flag('tags'); } if (this.options.depth) { command.option('depth', this.options.depth.toString()); } if (typeof this.options.recurseSubmodules !== 'undefined') { if (typeof this.options.recurseSubmodules === 'boolean') { command.flag(this.options.recurseSubmodules ? 'recurse-submodules' : 'no-recurse-submodules'); } else { command.option('recurse-submodules', this.options.recurseSubmodules); } } if (this.options.progress) { command.flag('progress'); } return command; } protected parseResult(result: CommandResult): FetchResult { const summary = { branches: [] as Array<{ name: string; oldRef?: string; newRef: string }>, tags: [] as Array<{ name: string; oldRef?: string; newRef: string }>, pruned: [] as string[] }; // Parse fetch output result.stdout.split('\n').forEach(line => { if (line.includes('->')) { // Parse branch/tag updates const match = line.match(/([a-f0-9]+)\.\.([a-f0-9]+)\s+(\S+)\s+->\s+(\S+)/); if (match) { const [, oldRef, newRef, localRef, remoteRef] = match; if (remoteRef.includes('refs/tags/')) { summary.tags.push({ name: remoteRef.replace('refs/tags/', ''), oldRef, newRef }); } else { summary.branches.push({ name: remoteRef.replace('refs/remotes/', ''), oldRef, newRef }); } } } else if (line.includes('[pruned]')) { // Parse pruned refs const pruneMatch = line.match(/\[pruned\] (.+)/); if (pruneMatch) { summary.pruned.push(pruneMatch[1]); } } }); return { remote: this.options.remote, summary: { branches: summary.branches.length > 0 ? summary.branches : undefined, tags: summary.tags.length > 0 ? summary.tags : undefined, pruned: summary.pruned.length > 0 ? summary.pruned : undefined }, raw: result.stdout }; } protected getCacheConfig() { return { command: 'fetch', stateType: RepoStateType.REMOTE }; } protected async validateOptions(): Promise<void> { if (this.options.remote && !this.options.all) { await RepositoryValidator.validateRemoteConfig( this.getResolvedPath(), this.options.remote, this.context.operation ); } if (this.options.depth !== undefined && this.options.depth <= 0) { throw ErrorHandler.handleValidationError( new Error('Depth must be a positive number'), { operation: this.context.operation } ); } } }

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/Sheshiyer/git-mcp-v2'

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