/**
* Git Branches Tool
*
* Branch management tool providing comprehensive Git branch operations.
* Supports both local Git operations and remote provider operations.
*
* Operations: create, list, get, delete, merge, compare
*/
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 GitBranchesParams extends ToolParams {
action: 'create' | 'list' | 'get' | 'delete' | 'merge' | 'compare';
// Branch operation parameters
branchName?: string; // For create, get, delete, merge
sourceBranch?: string; // For create (branch from)
targetBranch?: string; // For merge (merge into)
baseBranch?: string; // For compare
compareBranch?: string; // For compare
// Options
force?: boolean; // For delete, merge
remote?: string; // For remote operations (default: origin)
checkout?: boolean; // For create (checkout after creation)
// Remote operation parameters
repo?: string; // For remote operations
}
export class GitBranchesTool {
private gitExecutor: GitCommandExecutor;
private providerHandler?: ProviderOperationHandler;
constructor(providerConfig?: ProviderConfig) {
this.gitExecutor = new GitCommandExecutor();
if (providerConfig) {
this.providerHandler = new ProviderOperationHandler(providerConfig);
}
}
/**
* Execute git-branches operation
*/
async execute(params: GitBranchesParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-branches', 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 = ParameterValidator.validateOperationParams('git-branches', params.action, 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
const isRemoteOperation = this.isRemoteOperation(params.action);
if (isRemoteOperation) {
return await this.executeRemoteOperation(params, startTime);
} else {
return await this.executeLocalOperation(params, startTime);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'EXECUTION_ERROR',
`Failed to execute ${params.action}: ${errorMessage}`,
params.action,
{ error: errorMessage },
['Check the error details and try again']
);
}
}
/**
* Execute local Git branch operations
*/
private async executeLocalOperation(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
switch (params.action) {
case 'create':
return await this.handleCreateBranch(params, startTime);
case 'list':
return await this.handleListBranches(params, startTime);
case 'get':
return await this.handleGetBranch(params, startTime);
case 'delete':
return await this.handleDeleteBranch(params, startTime);
case 'merge':
return await this.handleMergeBranch(params, startTime);
case 'compare':
return await this.handleCompareBranches(params, startTime);
default:
return OperationErrorHandler.createToolError(
'UNSUPPORTED_OPERATION',
`Local operation '${params.action}' is not supported`,
params.action,
{},
['Use one of: create, list, get, delete, merge, compare']
);
}
}
/**
* Execute remote provider operations
*/
private async executeRemoteOperation(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
if (!this.providerHandler) {
return OperationErrorHandler.createToolError(
'PROVIDER_NOT_CONFIGURED',
'Provider handler is not configured for remote operations',
params.action,
{},
['Configure GitHub or Gitea provider to use remote operations']
);
}
if (!params.provider) {
if (configManager.isUniversalMode()) {
params.provider = 'both';
console.error(`[Universal Mode] Auto-applying both providers for ${params.action}`);
} else {
return OperationErrorHandler.createToolError(
'PROVIDER_REQUIRED',
'Provider parameter is required for remote operations',
params.action,
{},
['Specify provider as: github, gitea, or both']
);
}
}
const operation: ProviderOperation = {
provider: params.provider,
operation: this.mapActionToProviderOperation(params.action),
parameters: this.extractRemoteParameters(params),
requiresAuth: true,
isRemoteOperation: true
};
try {
const result = await this.providerHandler.executeOperation(operation);
return {
success: result.success,
data: result.partialFailure ? result : result.results[0]?.data,
error: result.success ? undefined : {
code: result.errors[0]?.error?.code || 'REMOTE_OPERATION_ERROR',
message: result.errors[0]?.error?.message || 'Remote operation failed',
details: result.errors,
suggestions: ['Check provider configuration and credentials']
},
metadata: {
provider: params.provider,
operation: params.action,
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_OPERATION_ERROR',
`Remote operation failed: ${errorMessage}`,
params.action,
{ error: errorMessage },
['Check provider configuration and network connectivity']
);
}
}
/**
* Handle create branch operation
*/
private async handleCreateBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
try {
if (!params.branchName) {
return OperationErrorHandler.createToolError(
'MISSING_PARAMETER',
'branchName is required for branch creation',
'create',
{},
['Provide a name for the new branch']
);
}
// Check if branch already exists
const existingBranches = await this.gitExecutor.listBranches(params.projectPath, { all: false });
if (existingBranches.success && existingBranches.branches?.includes(params.branchName)) {
return OperationErrorHandler.createToolError(
'BRANCH_EXISTS',
`Branch '${params.branchName}' already exists`,
'create',
{ branchName: params.branchName },
['Use a different branch name or delete the existing branch first']
);
}
// Create branch
const result = await this.gitExecutor.createBranch(
params.projectPath,
params.branchName,
params.sourceBranch
);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'create branch', params.projectPath);
}
// Checkout branch if requested
let checkoutResult: GitCommandResult | null = null;
if (params.checkout) {
checkoutResult = await this.gitExecutor.checkoutBranch(params.projectPath, params.branchName);
if (!checkoutResult.success) {
return OperationErrorHandler.handleGitError(checkoutResult.stderr, 'checkout branch', params.projectPath);
}
}
return {
success: true,
data: {
message: `Branch '${params.branchName}' created successfully`,
branchName: params.branchName,
sourceBranch: params.sourceBranch || 'current branch',
checkedOut: params.checkout || false,
output: result.stdout,
checkoutOutput: checkoutResult?.stdout
},
metadata: {
operation: 'create',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'CREATE_BRANCH_ERROR',
`Failed to create branch: ${errorMessage}`,
'create',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Handle list branches operation
*/
private async handleListBranches(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
try {
const result = await this.gitExecutor.listBranches(params.projectPath, {
all: true,
remote: params.remote
});
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'list branches', params.projectPath);
}
// Get current branch
const currentBranchResult = await this.gitExecutor.getCurrentBranch(params.projectPath);
const currentBranch = currentBranchResult.success ? currentBranchResult.branch : null;
return {
success: true,
data: {
branches: result.branches || [],
remoteBranches: result.remoteBranches || [],
currentBranch,
total: (result.branches?.length || 0) + (result.remoteBranches?.length || 0),
raw: result.stdout
},
metadata: {
operation: 'list',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'LIST_BRANCHES_ERROR',
`Failed to list branches: ${errorMessage}`,
'list',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Handle get branch operation
*/
private async handleGetBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
try {
if (!params.branchName) {
return OperationErrorHandler.createToolError(
'MISSING_PARAMETER',
'branchName is required for get branch operation',
'get',
{},
['Provide the name of the branch to get information about']
);
}
// Get branch information
const branchInfo = await this.gitExecutor.getBranchInfo(params.projectPath, params.branchName);
if (!branchInfo.success) {
return OperationErrorHandler.handleGitError(branchInfo.stderr, 'get branch info', params.projectPath);
}
// Get branch commits
const commitsResult = await this.gitExecutor.getBranchCommits(params.projectPath, params.branchName, 10);
return {
success: true,
data: {
branchName: params.branchName,
exists: branchInfo.exists,
isRemote: branchInfo.isRemote,
lastCommit: branchInfo.lastCommit,
commits: commitsResult.success ? commitsResult.commits : [],
upstream: branchInfo.upstream,
ahead: branchInfo.ahead,
behind: branchInfo.behind
},
metadata: {
operation: 'get',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'GET_BRANCH_ERROR',
`Failed to get branch information: ${errorMessage}`,
'get',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Handle delete branch operation
*/
private async handleDeleteBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
try {
if (!params.branchName) {
return OperationErrorHandler.createToolError(
'MISSING_PARAMETER',
'branchName is required for branch deletion',
'delete',
{},
['Provide the name of the branch to delete']
);
}
// Check if trying to delete current branch
const currentBranchResult = await this.gitExecutor.getCurrentBranch(params.projectPath);
if (currentBranchResult.success && currentBranchResult.branch === params.branchName) {
return OperationErrorHandler.createToolError(
'CANNOT_DELETE_CURRENT_BRANCH',
`Cannot delete current branch '${params.branchName}'`,
'delete',
{ branchName: params.branchName },
['Switch to a different branch before deleting this one']
);
}
const result = await this.gitExecutor.deleteBranch(
params.projectPath,
params.branchName,
params.force || false
);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'delete branch', params.projectPath);
}
return {
success: true,
data: {
message: `Branch '${params.branchName}' deleted successfully`,
branchName: params.branchName,
forced: params.force || false,
output: result.stdout
},
metadata: {
operation: 'delete',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'DELETE_BRANCH_ERROR',
`Failed to delete branch: ${errorMessage}`,
'delete',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Handle merge branch operation
*/
private async handleMergeBranch(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
try {
if (!params.branchName) {
return OperationErrorHandler.createToolError(
'MISSING_PARAMETER',
'branchName is required for branch merge',
'merge',
{},
['Provide the name of the branch to merge']
);
}
// Get current branch (target branch)
const currentBranchResult = await this.gitExecutor.getCurrentBranch(params.projectPath);
if (!currentBranchResult.success) {
return OperationErrorHandler.handleGitError(currentBranchResult.stderr, 'get current branch', params.projectPath);
}
const targetBranch = params.targetBranch || currentBranchResult.branch;
// Check for uncommitted changes
const statusResult = await this.gitExecutor.getStatus(params.projectPath);
if (statusResult.success && statusResult.parsedStatus &&
(statusResult.parsedStatus.modified.length > 0 || statusResult.parsedStatus.added.length > 0)) {
return OperationErrorHandler.createToolError(
'UNCOMMITTED_CHANGES',
'Cannot merge with uncommitted changes',
'merge',
{ uncommittedFiles: [...statusResult.parsedStatus.modified, ...statusResult.parsedStatus.added] },
['Commit or stash your changes before merging']
);
}
const result = await this.gitExecutor.mergeBranch(
params.projectPath,
params.branchName,
{ force: params.force }
);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'merge branch', params.projectPath);
}
return {
success: true,
data: {
message: `Branch '${params.branchName}' merged into '${targetBranch}' successfully`,
sourceBranch: params.branchName,
targetBranch,
mergeCommit: result.mergeCommit,
conflicts: result.conflicts || [],
output: result.stdout
},
metadata: {
operation: 'merge',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'MERGE_BRANCH_ERROR',
`Failed to merge branch: ${errorMessage}`,
'merge',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Handle compare branches operation
*/
private async handleCompareBranches(params: GitBranchesParams, startTime: number): Promise<ToolResult> {
try {
if (!params.baseBranch || !params.compareBranch) {
return OperationErrorHandler.createToolError(
'MISSING_PARAMETER',
'baseBranch and compareBranch are required for comparison',
'compare',
{},
['Provide both branch names to compare']
);
}
const result = await this.gitExecutor.compareBranches(
params.projectPath,
params.baseBranch,
params.compareBranch
);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'compare branches', params.projectPath);
}
return {
success: true,
data: {
baseBranch: params.baseBranch,
compareBranch: params.compareBranch,
ahead: result.ahead || 0,
behind: result.behind || 0,
commits: result.commits || [],
files: result.files || [],
summary: {
totalCommits: result.commits?.length || 0,
filesChanged: result.files?.length || 0,
insertions: result.insertions || 0,
deletions: result.deletions || 0
}
},
metadata: {
operation: 'compare',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'COMPARE_BRANCHES_ERROR',
`Failed to compare branches: ${errorMessage}`,
'compare',
{ error: errorMessage, projectPath: params.projectPath }
);
}
}
/**
* Check if operation is a remote operation
*/
private isRemoteOperation(action: string): boolean {
// For now, all branch operations are local
// Remote branch operations would be handled through git-workflow or specific remote tools
return false;
}
/**
* Extract parameters for remote operations
*/
private extractRemoteParameters(params: GitBranchesParams): Record<string, any> {
const remoteParams: Record<string, any> = {
projectPath: params.projectPath
};
if (params.repo) remoteParams.repo = params.repo;
if (params.branchName) remoteParams.branchName = params.branchName;
return remoteParams;
}
/**
* Map git-branches actions to provider operations
*/
private mapActionToProviderOperation(action: string): string {
const actionMap: Record<string, string> = {
'list': 'branch-list',
'get': 'branch-get',
'create': 'branch-create',
'delete': 'branch-delete'
};
return actionMap[action] || action;
}
/**
* Get tool schema for MCP registration
*/
static getToolSchema() {
return {
name: 'git-branches',
description: 'Git branch management tool for branch lifecycle operations. Supports create, list, get, delete, merge, and compare operations. Includes safety warnings for destructive operations like branch deletion. In universal mode (GIT_MCP_MODE=universal), automatically executes on both GitHub and Gitea providers.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['create', 'list', 'get', 'delete', 'merge', 'compare'],
description: 'The branch operation to perform. WARNING: delete operation permanently removes branches and their commit history.'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
provider: {
type: 'string',
enum: ['github', 'gitea', 'both'],
description: 'Provider for remote operations (if supported)'
},
branchName: {
type: 'string',
description: 'Name of the branch (required for create, get, delete, merge)'
},
sourceBranch: {
type: 'string',
description: 'Source branch to create from (for create action)'
},
targetBranch: {
type: 'string',
description: 'Target branch to merge into (for merge action)'
},
baseBranch: {
type: 'string',
description: 'Base branch for comparison (required for compare action)'
},
compareBranch: {
type: 'string',
description: 'Branch to compare against base (required for compare action)'
},
force: {
type: 'boolean',
description: 'Force operation (for delete, merge actions)'
},
remote: {
type: 'string',
description: 'Remote name (default: origin)'
},
checkout: {
type: 'boolean',
description: 'Checkout branch after creation (for create action)'
},
repo: {
type: 'string',
description: 'Repository name (for remote operations)'
}
},
required: ['action', 'projectPath'],
additionalProperties: false
},
errorCodes: {
'VALIDATION_ERROR': 'Parameter validation failed - check required parameters and format',
'BRANCH_EXISTS': 'Branch already exists - use different name or delete existing branch',
'BRANCH_NOT_FOUND': 'Branch not found - check branch name and remote configuration',
'MERGE_CONFLICT': 'Merge conflicts detected - resolve conflicts before completing merge',
'DIRTY_WORKING_TREE': 'Working directory has uncommitted changes - commit or stash first',
'NOT_A_GIT_REPOSITORY': 'Directory is not a Git repository - use git init first',
'PERMISSION_DENIED': 'Permission denied - check access rights and credentials'
},
warnings: {
'delete': 'Branch deletion is permanent and cannot be undone. Ensure branch is merged or no longer needed.',
'force': 'Force operations can cause data loss. Use with extreme caution.'
},
examples: [
{
description: 'List all branches',
input: {
action: 'list',
projectPath: '/path/to/project'
}
},
{
description: 'Create new feature branch',
input: {
action: 'create',
projectPath: '/path/to/project',
branchName: 'feature/new-feature',
sourceBranch: 'main',
checkout: true
}
},
{
description: 'Compare two branches',
input: {
action: 'compare',
projectPath: '/path/to/project',
baseBranch: 'main',
compareBranch: 'feature/new-feature'
}
}
]
};
}
}