/**
* Git Sync Tool
*
* Advanced synchronization tool providing intelligent sync and status operations.
* Supports both local Git synchronization and remote provider synchronization.
*
* Operations: sync, status
*/
import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js';
import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js';
import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js';
import { ProviderOperationHandler } from '../providers/provider-operation-handler.js';
import { ProviderConfig, ProviderOperation } from '../providers/types.js';
import { configManager } from '../config.js';
export interface GitSyncParams extends ToolParams {
action: 'sync' | 'status';
// Sync parameters
remote?: string; // Remote to sync with (default: origin)
branch?: string; // Branch to sync (default: current branch)
strategy?: 'merge' | 'rebase' | 'fast-forward'; // Sync strategy
force?: boolean; // Force sync (use with caution)
dryRun?: boolean; // Show what would be done without executing
// Status parameters
detailed?: boolean; // Show detailed sync status
includeRemote?: boolean; // Include remote status information
checkAhead?: boolean; // Check commits ahead/behind
// Provider parameters (for remote sync)
repo?: string; // Repository name (auto-detected if not provided)
}
export interface SyncStatus {
localBranch: string;
remoteBranch: string;
ahead: number;
behind: number;
hasUncommittedChanges: boolean;
hasUntrackedFiles: boolean;
lastSync?: string;
conflicts?: string[];
remoteStatus?: {
exists: boolean;
lastCommit?: string;
lastCommitDate?: string;
};
}
export interface SyncResult {
success: boolean;
status: SyncStatus;
operations: string[];
conflicts?: string[];
warnings?: string[];
}
export class GitSyncTool {
private gitExecutor: GitCommandExecutor;
private providerHandler?: ProviderOperationHandler;
constructor(providerConfig?: ProviderConfig) {
this.gitExecutor = new GitCommandExecutor();
if (providerConfig) {
this.providerHandler = new ProviderOperationHandler(providerConfig);
}
}
/**
* Execute git-sync operation
*/
async execute(params: GitSyncParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-sync', params);
if (!validation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Parameter validation failed: ${validation.errors.join(', ')}`,
params.action,
{ validationErrors: validation.errors },
validation.suggestions
);
}
// Validate operation-specific parameters
const operationValidation = this.validateOperationParams(params);
if (!operationValidation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Operation validation failed: ${operationValidation.errors.join(', ')}`,
params.action,
{ validationErrors: operationValidation.errors },
operationValidation.suggestions
);
}
// Route to appropriate handler
switch (params.action) {
case 'sync':
return await this.handleSync(params, startTime);
case 'status':
return await this.handleStatus(params, startTime);
default:
return OperationErrorHandler.createToolError(
'UNSUPPORTED_OPERATION',
`Unsupported operation: ${params.action}`,
params.action,
{ supportedOperations: ['sync', 'status'] },
['Use one of the supported operations: sync, status']
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'EXECUTION_ERROR',
`Failed to execute git-sync operation: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Validate operation-specific parameters
*/
private validateOperationParams(params: GitSyncParams): { isValid: boolean; errors: string[]; suggestions: string[] } {
const errors: string[] = [];
const suggestions: string[] = [];
// Validate sync parameters
if (params.action === 'sync') {
if (params.strategy && !['merge', 'rebase', 'fast-forward'].includes(params.strategy)) {
errors.push('Invalid sync strategy. Must be one of: merge, rebase, fast-forward');
suggestions.push('Use strategy: "merge", "rebase", or "fast-forward"');
}
}
return {
isValid: errors.length === 0,
errors,
suggestions
};
}
/**
* Handle sync operation
*/
private async handleSync(params: GitSyncParams, startTime: number): Promise<ToolResult> {
try {
const remote = params.remote || 'origin';
const branch = params.branch || await this.getCurrentBranch(params.projectPath);
const strategy = params.strategy || 'merge';
const operations: string[] = [];
// Get current status first
const statusResult = await this.getCurrentStatus(params);
if (!statusResult.success) {
return OperationErrorHandler.createToolError(
'STATUS_CHECK_FAILED',
'Failed to get current repository status',
params.action,
{ statusError: statusResult }
);
}
const status = statusResult.status;
// Check for uncommitted changes
if (status.hasUncommittedChanges && !params.force) {
return OperationErrorHandler.createToolError(
'UNCOMMITTED_CHANGES',
'Repository has uncommitted changes. Commit or stash changes before syncing.',
params.action,
{ status },
[
'Commit changes: git add . && git commit -m "message"',
'Stash changes: git stash (use git stash pop later to restore)',
'Use git-stash tool to save changes temporarily',
'Use force: true to override (not recommended - may cause data loss)'
]
);
}
// Dry run - show what would be done
if (params.dryRun) {
const plannedOperations = await this.planSyncOperations(params, status);
return {
success: true,
data: {
dryRun: true,
plannedOperations,
currentStatus: status
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
// Fetch latest changes
const fetchArgs = [remote];
if (branch) {
// Only fetch specific branch if it's different from current branch
const currentBranch = await this.getCurrentBranch(params.projectPath);
if (currentBranch !== branch) {
fetchArgs.push(`${branch}:${branch}`);
}
}
const fetchResult = await this.gitExecutor.executeGitCommand('fetch', fetchArgs, params.projectPath);
if (!fetchResult.success) {
return OperationErrorHandler.createToolError(
'FETCH_FAILED',
`Failed to fetch from remote: ${fetchResult.stderr}`,
params.action,
{ fetchResult }
);
}
operations.push(`Fetched from ${remote}`);
// Get updated status after fetch
const updatedStatus = await this.getCurrentStatus(params);
if (!updatedStatus.success) {
return OperationErrorHandler.createToolError(
'STATUS_UPDATE_FAILED',
'Failed to get updated status after fetch',
params.action
);
}
const newStatus = updatedStatus.status;
// Perform sync based on strategy
const syncResult = await this.performSync(params, newStatus, strategy, operations);
// Handle remote sync if provider is available
if (this.providerHandler && params.provider) {
const remoteSync = await this.performRemoteSync(params);
if (remoteSync.success) {
operations.push('Remote synchronization completed');
} else {
syncResult.warnings = syncResult.warnings || [];
syncResult.warnings.push('Remote synchronization failed but local sync succeeded');
}
}
return {
success: syncResult.success,
data: {
syncResult,
operations,
finalStatus: await this.getCurrentStatus(params)
},
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'SYNC_FAILED',
`Sync operation failed: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Handle status operation
*/
private async handleStatus(params: GitSyncParams, startTime: number): Promise<ToolResult> {
try {
const statusResult = await this.getCurrentStatus(params);
if (!statusResult.success) {
return OperationErrorHandler.createToolError(
'STATUS_FAILED',
'Failed to get repository status',
params.action,
{ statusResult }
);
}
let data: any = {
status: statusResult.status
};
// Include remote status if requested
if (params.includeRemote && this.providerHandler && params.provider) {
const remoteStatus = await this.getRemoteStatus(params);
data.remoteStatus = remoteStatus;
}
// Include detailed information if requested
if (params.detailed) {
const detailedInfo = await this.getDetailedStatus(params);
data.detailed = detailedInfo;
}
return {
success: true,
data,
metadata: {
operation: params.action,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'STATUS_FAILED',
`Status operation failed: ${errorMessage}`,
params.action,
{ error: errorMessage }
);
}
}
/**
* Get current repository status
*/
private async getCurrentStatus(params: GitSyncParams): Promise<{ success: boolean; status: SyncStatus }> {
try {
const remote = params.remote || 'origin';
const branch = params.branch || await this.getCurrentBranch(params.projectPath);
const remoteBranch = `${remote}/${branch}`;
// Check if we have uncommitted changes
const statusResult = await this.gitExecutor.executeGitCommand('status', ['--porcelain'], params.projectPath);
const hasUncommittedChanges = statusResult.success && statusResult.stdout.trim().length > 0;
// Check for untracked files
const untrackedResult = await this.gitExecutor.executeGitCommand('ls-files', ['--others', '--exclude-standard'], params.projectPath);
const hasUntrackedFiles = untrackedResult.success && untrackedResult.stdout.trim().length > 0;
// Get ahead/behind information
let ahead = 0;
let behind = 0;
if (params.checkAhead !== false) {
const revListResult = await this.gitExecutor.executeGitCommand('rev-list', ['--left-right', '--count', `${remoteBranch}...HEAD`], params.projectPath);
if (revListResult.success) {
const counts = revListResult.stdout.trim().split('\t');
if (counts.length === 2) {
behind = parseInt(counts[0]) || 0;
ahead = parseInt(counts[1]) || 0;
}
}
}
const status: SyncStatus = {
localBranch: branch || 'main',
remoteBranch,
ahead,
behind,
hasUncommittedChanges: !!hasUncommittedChanges,
hasUntrackedFiles: !!hasUntrackedFiles
};
return { success: true, status };
} catch (error) {
return { success: false, status: {} as SyncStatus };
}
}
/**
* Plan sync operations for dry run
*/
private async planSyncOperations(params: GitSyncParams, status: SyncStatus): Promise<string[]> {
const operations: string[] = [];
const remote = params.remote || 'origin';
const strategy = params.strategy || 'merge';
operations.push(`Fetch from ${remote}`);
if (status.behind > 0) {
switch (strategy) {
case 'merge':
operations.push(`Merge ${status.behind} commits from ${status.remoteBranch}`);
break;
case 'rebase':
operations.push(`Rebase ${status.behind} commits from ${status.remoteBranch}`);
break;
case 'fast-forward':
operations.push(`Fast-forward ${status.behind} commits from ${status.remoteBranch}`);
break;
}
}
if (status.ahead > 0) {
operations.push(`Push ${status.ahead} local commits to ${status.remoteBranch}`);
}
if (status.behind === 0 && status.ahead === 0) {
operations.push('Repository is already up to date');
}
return operations;
}
/**
* Perform actual sync operation
*/
private async performSync(params: GitSyncParams, status: SyncStatus, strategy: string, operations: string[]): Promise<SyncResult> {
const result: SyncResult = {
success: true,
status,
operations: [...operations]
};
try {
// Handle incoming changes (behind)
if (status.behind > 0) {
let syncCommand: string[];
switch (strategy) {
case 'merge':
syncCommand = ['merge', status.remoteBranch];
break;
case 'rebase':
syncCommand = ['rebase', status.remoteBranch];
break;
case 'fast-forward':
syncCommand = ['merge', '--ff-only', status.remoteBranch];
break;
default:
syncCommand = ['merge', status.remoteBranch];
}
const syncResult = await this.gitExecutor.executeGitCommand(syncCommand[0], syncCommand.slice(1), params.projectPath);
if (!syncResult.success) {
result.success = false;
result.conflicts = [syncResult.stderr || 'Sync failed'];
return result;
}
result.operations.push(`${strategy} completed: ${status.behind} commits integrated`);
}
// Handle outgoing changes (ahead)
if (status.ahead > 0) {
const pushResult = await this.gitExecutor.executeGitCommand('push', [], params.projectPath);
if (!pushResult.success) {
result.warnings = result.warnings || [];
result.warnings.push(`Failed to push local commits: ${pushResult.stderr}`);
} else {
result.operations.push(`Pushed ${status.ahead} commits to remote`);
}
}
return result;
} catch (error) {
result.success = false;
result.conflicts = [error instanceof Error ? error.message : 'Unknown sync error'];
return result;
}
}
/**
* Perform remote synchronization using provider
*/
private async performRemoteSync(params: GitSyncParams): Promise<{ success: boolean; error?: string }> {
if (!this.providerHandler || !params.provider) {
return { success: false, error: 'Provider not configured' };
}
try {
const operation: ProviderOperation = {
provider: params.provider,
operation: 'sync',
parameters: {
repo: params.repo,
branch: params.branch
},
requiresAuth: true,
isRemoteOperation: true
};
const result = await this.providerHandler.executeOperation(operation);
return { success: result.success };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Remote sync failed'
};
}
}
/**
* Get remote status information
*/
private async getRemoteStatus(params: GitSyncParams): Promise<any> {
if (!this.providerHandler || !params.provider) {
return { error: 'Provider not configured' };
}
try {
const operation: ProviderOperation = {
provider: params.provider,
operation: 'get',
parameters: {
repo: params.repo
},
requiresAuth: true,
isRemoteOperation: true
};
const result = await this.providerHandler.executeOperation(operation);
return result.success ? result.results[0]?.data : { error: 'Failed to get remote status' };
} catch (error) {
return { error: error instanceof Error ? error.message : 'Remote status failed' };
}
}
/**
* Get detailed status information
*/
private async getDetailedStatus(params: GitSyncParams): Promise<any> {
try {
// Get commit history
const logResult = await this.gitExecutor.executeGitCommand('log', ['--oneline', '-10'], params.projectPath);
// Get branch information
const branchResult = await this.gitExecutor.executeGitCommand('branch', ['-vv'], params.projectPath);
// Get remote information
const remoteResult = await this.gitExecutor.executeGitCommand('remote', ['-v'], params.projectPath);
return {
recentCommits: logResult.success ? logResult.stdout.split('\n').filter((line: string) => line.trim()) : [],
branches: branchResult.success ? branchResult.stdout.split('\n').filter((line: string) => line.trim()) : [],
remotes: remoteResult.success ? remoteResult.stdout.split('\n').filter((line: string) => line.trim()) : []
};
} catch (error) {
return { error: error instanceof Error ? error.message : 'Failed to get detailed status' };
}
}
/**
* Get tool schema for MCP registration
*/
static getToolSchema() {
return {
name: 'git-sync',
description: 'Advanced Git synchronization tool for intelligent sync and status operations. Supports both local Git synchronization and remote provider synchronization.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['sync', 'status'],
description: 'The Git sync operation to perform'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
provider: {
type: 'string',
enum: ['github', 'gitea', 'both'],
description: 'Provider for remote operations (optional for local-only operations)'
},
remote: {
type: 'string',
description: 'Remote to sync with (default: origin)'
},
branch: {
type: 'string',
description: 'Branch to sync (default: current branch)'
},
strategy: {
type: 'string',
enum: ['merge', 'rebase', 'fast-forward'],
description: 'Sync strategy (default: merge)'
},
force: {
type: 'boolean',
description: 'Force sync (use with caution, may override uncommitted changes)'
},
dryRun: {
type: 'boolean',
description: 'Show what would be done without executing (for sync operation)'
},
detailed: {
type: 'boolean',
description: 'Show detailed sync status (for status operation)'
},
includeRemote: {
type: 'boolean',
description: 'Include remote status information (for status operation)'
},
checkAhead: {
type: 'boolean',
description: 'Check commits ahead/behind (default: true for status operation)'
},
repo: {
type: 'string',
description: 'Repository name (auto-detected if not provided, for remote operations)'
}
},
required: ['action', 'projectPath']
}
};
}
/**
* Get current branch name
*/
private async getCurrentBranch(projectPath: string): Promise<string | null> {
try {
const result = await this.gitExecutor.executeGitCommand('branch', ['--show-current'], projectPath);
if (result.success && result.stdout.trim()) {
return result.stdout.trim();
}
return null;
} catch (error) {
return null;
}
}
}