#
tokens: 16124/50000 3/34 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/sheshiyer/git-mcp-v2?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── jest.config.js
├── package.json
├── README.md
├── src
│   ├── caching
│   │   ├── cache.ts
│   │   └── repository-cache.ts
│   ├── common
│   │   └── command-builder.ts
│   ├── errors
│   │   ├── error-handler.ts
│   │   └── error-types.ts
│   ├── git-operations.ts
│   ├── index.ts
│   ├── monitoring
│   │   ├── performance.ts
│   │   └── types.ts
│   ├── operations
│   │   ├── base
│   │   │   ├── base-operation.ts
│   │   │   └── operation-result.ts
│   │   ├── branch
│   │   │   ├── branch-operations.ts
│   │   │   └── branch-types.ts
│   │   ├── remote
│   │   │   ├── remote-operations.ts
│   │   │   └── remote-types.ts
│   │   ├── repository
│   │   │   └── repository-operations.ts
│   │   ├── sync
│   │   │   ├── sync-operations.ts
│   │   │   └── sync-types.ts
│   │   ├── tag
│   │   │   ├── tag-operations.ts
│   │   │   └── tag-types.ts
│   │   └── working-tree
│   │       ├── working-tree-operations.ts
│   │       └── working-tree-types.ts
│   ├── tool-handler.ts
│   ├── types.ts
│   └── utils
│       ├── command.ts
│       ├── logger.ts
│       ├── path.ts
│       ├── paths.ts
│       └── repository.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/operations/sync/sync-operations.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BaseGitOperation } from '../base/base-operation.js';
  2 | import { GitCommandBuilder } from '../../common/command-builder.js';
  3 | import { CommandResult } from '../base/operation-result.js';
  4 | import { ErrorHandler } from '../../errors/error-handler.js';
  5 | import { RepositoryValidator } from '../../utils/repository.js';
  6 | import { CommandExecutor } from '../../utils/command.js';
  7 | import { RepoStateType } from '../../caching/repository-cache.js';
  8 | import {
  9 |   PushOptions,
 10 |   PullOptions,
 11 |   FetchOptions,
 12 |   PushResult,
 13 |   PullResult,
 14 |   FetchResult
 15 | } from './sync-types.js';
 16 | 
 17 | /**
 18 |  * Handles Git push operations
 19 |  */
 20 | export class PushOperation extends BaseGitOperation<PushOptions, PushResult> {
 21 |   protected buildCommand(): GitCommandBuilder {
 22 |     const command = GitCommandBuilder.push();
 23 | 
 24 |     if (this.options.remote) {
 25 |       command.arg(this.options.remote);
 26 |     }
 27 | 
 28 |     if (this.options.branch) {
 29 |       command.arg(this.options.branch);
 30 |     }
 31 | 
 32 |     if (this.options.force) {
 33 |       command.withForce();
 34 |     }
 35 | 
 36 |     if (this.options.forceWithLease) {
 37 |       command.flag('force-with-lease');
 38 |     }
 39 | 
 40 |     if (this.options.all) {
 41 |       command.flag('all');
 42 |     }
 43 | 
 44 |     if (this.options.tags) {
 45 |       command.flag('tags');
 46 |     }
 47 | 
 48 |     if (this.options.noVerify) {
 49 |       command.withNoVerify();
 50 |     }
 51 | 
 52 |     if (this.options.setUpstream) {
 53 |       command.withSetUpstream();
 54 |     }
 55 | 
 56 |     if (this.options.prune) {
 57 |       command.flag('prune');
 58 |     }
 59 | 
 60 |     return command;
 61 |   }
 62 | 
 63 |   protected parseResult(result: CommandResult): PushResult {
 64 |     const summary = {
 65 |       created: [] as string[],
 66 |       deleted: [] as string[],
 67 |       updated: [] as string[],
 68 |       rejected: [] as string[]
 69 |     };
 70 | 
 71 |     // Parse push output
 72 |     result.stdout.split('\n').forEach(line => {
 73 |       if (line.startsWith('To ')) return; // Skip remote URL line
 74 | 
 75 |       const match = line.match(/^\s*([a-f0-9]+)\.\.([a-f0-9]+)\s+(\S+)\s+->\s+(\S+)/);
 76 |       if (match) {
 77 |         const [, oldRef, newRef, localRef, remoteRef] = match;
 78 |         summary.updated.push(remoteRef);
 79 |       } else if (line.includes('[new branch]')) {
 80 |         const branchMatch = line.match(/\[new branch\]\s+(\S+)\s+->\s+(\S+)/);
 81 |         if (branchMatch) {
 82 |           summary.created.push(branchMatch[2]);
 83 |         }
 84 |       } else if (line.includes('[deleted]')) {
 85 |         const deleteMatch = line.match(/\[deleted\]\s+(\S+)/);
 86 |         if (deleteMatch) {
 87 |           summary.deleted.push(deleteMatch[1]);
 88 |         }
 89 |       } else if (line.includes('! [rejected]')) {
 90 |         const rejectMatch = line.match(/\! \[rejected\]\s+(\S+)/);
 91 |         if (rejectMatch) {
 92 |           summary.rejected.push(rejectMatch[1]);
 93 |         }
 94 |       }
 95 |     });
 96 | 
 97 |     return {
 98 |       remote: this.options.remote || 'origin',
 99 |       branch: this.options.branch,
100 |       forced: this.options.force || false,
101 |       summary: {
102 |         created: summary.created.length > 0 ? summary.created : undefined,
103 |         deleted: summary.deleted.length > 0 ? summary.deleted : undefined,
104 |         updated: summary.updated.length > 0 ? summary.updated : undefined,
105 |         rejected: summary.rejected.length > 0 ? summary.rejected : undefined
106 |       },
107 |       raw: result.stdout
108 |     };
109 |   }
110 | 
111 |   protected getCacheConfig() {
112 |     return {
113 |       command: 'push',
114 |       stateType: RepoStateType.REMOTE
115 |     };
116 |   }
117 | 
118 |   protected async validateOptions(): Promise<void> {
119 |     if (!this.options.branch && !this.options.all) {
120 |       throw ErrorHandler.handleValidationError(
121 |         new Error('Either branch or --all must be specified'),
122 |         { operation: this.context.operation }
123 |       );
124 |     }
125 | 
126 |     if (this.options.remote) {
127 |       await RepositoryValidator.validateRemoteConfig(
128 |         this.getResolvedPath(),
129 |         this.options.remote,
130 |         this.context.operation
131 |       );
132 |     }
133 | 
134 |     if (this.options.branch) {
135 |       await RepositoryValidator.validateBranchExists(
136 |         this.getResolvedPath(),
137 |         this.options.branch,
138 |         this.context.operation
139 |       );
140 |     }
141 |   }
142 | }
143 | 
144 | /**
145 |  * Handles Git pull operations
146 |  */
147 | export class PullOperation extends BaseGitOperation<PullOptions, PullResult> {
148 |   protected buildCommand(): GitCommandBuilder {
149 |     const command = GitCommandBuilder.pull();
150 | 
151 |     if (this.options.remote) {
152 |       command.arg(this.options.remote);
153 |     }
154 | 
155 |     if (this.options.branch) {
156 |       command.arg(this.options.branch);
157 |     }
158 | 
159 |     if (this.options.rebase) {
160 |       command.flag('rebase');
161 |     }
162 | 
163 |     if (this.options.autoStash) {
164 |       command.flag('autostash');
165 |     }
166 | 
167 |     if (this.options.allowUnrelated) {
168 |       command.flag('allow-unrelated-histories');
169 |     }
170 | 
171 |     if (this.options.ff === 'only') {
172 |       command.flag('ff-only');
173 |     } else if (this.options.ff === 'no') {
174 |       command.flag('no-ff');
175 |     }
176 | 
177 |     if (this.options.strategy) {
178 |       command.option('strategy', this.options.strategy);
179 |     }
180 | 
181 |     if (this.options.strategyOption) {
182 |       this.options.strategyOption.forEach(opt => {
183 |         command.option('strategy-option', opt);
184 |       });
185 |     }
186 | 
187 |     return command;
188 |   }
189 | 
190 |   protected parseResult(result: CommandResult): PullResult {
191 |     const summary = {
192 |       merged: [] as string[],
193 |       conflicts: [] as string[]
194 |     };
195 | 
196 |     let filesChanged = 0;
197 |     let insertions = 0;
198 |     let deletions = 0;
199 | 
200 |     // Parse pull output
201 |     result.stdout.split('\n').forEach(line => {
202 |       if (line.includes('|')) {
203 |         // Parse merge stats
204 |         const statsMatch = line.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
205 |         if (statsMatch) {
206 |           filesChanged = parseInt(statsMatch[1], 10);
207 |           insertions = statsMatch[2] ? parseInt(statsMatch[2], 10) : 0;
208 |           deletions = statsMatch[3] ? parseInt(statsMatch[3], 10) : 0;
209 |         }
210 |       } else if (line.includes('Fast-forward') || line.includes('Merge made by')) {
211 |         // Track merged files
212 |         const mergeMatch = line.match(/([^/]+)$/);
213 |         if (mergeMatch) {
214 |           summary.merged.push(mergeMatch[1]);
215 |         }
216 |       } else if (line.includes('CONFLICT')) {
217 |         // Track conflicts
218 |         const conflictMatch = line.match(/CONFLICT \(.+?\): (.+)/);
219 |         if (conflictMatch) {
220 |           summary.conflicts.push(conflictMatch[1]);
221 |         }
222 |       }
223 |     });
224 | 
225 |     return {
226 |       remote: this.options.remote || 'origin',
227 |       branch: this.options.branch,
228 |       rebased: this.options.rebase || false,
229 |       filesChanged,
230 |       insertions,
231 |       deletions,
232 |       summary: {
233 |         merged: summary.merged.length > 0 ? summary.merged : undefined,
234 |         conflicts: summary.conflicts.length > 0 ? summary.conflicts : undefined
235 |       },
236 |       raw: result.stdout
237 |     };
238 |   }
239 | 
240 |   protected getCacheConfig() {
241 |     return {
242 |       command: 'pull',
243 |       stateType: RepoStateType.REMOTE
244 |     };
245 |   }
246 | 
247 |   protected async validateOptions(): Promise<void> {
248 |     if (!this.options.branch) {
249 |       throw ErrorHandler.handleValidationError(
250 |         new Error('Branch must be specified'),
251 |         { operation: this.context.operation }
252 |       );
253 |     }
254 | 
255 |     if (this.options.remote) {
256 |       await RepositoryValidator.validateRemoteConfig(
257 |         this.getResolvedPath(),
258 |         this.options.remote,
259 |         this.context.operation
260 |       );
261 |     }
262 | 
263 |     // Ensure working tree is clean unless autostash is enabled
264 |     if (!this.options.autoStash) {
265 |       await RepositoryValidator.ensureClean(
266 |         this.getResolvedPath(),
267 |         this.context.operation
268 |       );
269 |     }
270 |   }
271 | }
272 | 
273 | /**
274 |  * Handles Git fetch operations
275 |  */
276 | export class FetchOperation extends BaseGitOperation<FetchOptions, FetchResult> {
277 |   protected buildCommand(): GitCommandBuilder {
278 |     const command = GitCommandBuilder.fetch();
279 | 
280 |     if (this.options.remote && !this.options.all) {
281 |       command.arg(this.options.remote);
282 |     }
283 | 
284 |     if (this.options.all) {
285 |       command.flag('all');
286 |     }
287 | 
288 |     if (this.options.prune) {
289 |       command.flag('prune');
290 |     }
291 | 
292 |     if (this.options.pruneTags) {
293 |       command.flag('prune-tags');
294 |     }
295 | 
296 |     if (this.options.tags) {
297 |       command.flag('tags');
298 |     }
299 | 
300 |     if (this.options.tagsOnly) {
301 |       command.flag('tags').flag('no-recurse-submodules');
302 |     }
303 | 
304 |     if (this.options.forceTags) {
305 |       command.flag('force').flag('tags');
306 |     }
307 | 
308 |     if (this.options.depth) {
309 |       command.option('depth', this.options.depth.toString());
310 |     }
311 | 
312 |     if (typeof this.options.recurseSubmodules !== 'undefined') {
313 |       if (typeof this.options.recurseSubmodules === 'boolean') {
314 |         command.flag(this.options.recurseSubmodules ? 'recurse-submodules' : 'no-recurse-submodules');
315 |       } else {
316 |         command.option('recurse-submodules', this.options.recurseSubmodules);
317 |       }
318 |     }
319 | 
320 |     if (this.options.progress) {
321 |       command.flag('progress');
322 |     }
323 | 
324 |     return command;
325 |   }
326 | 
327 |   protected parseResult(result: CommandResult): FetchResult {
328 |     const summary = {
329 |       branches: [] as Array<{ name: string; oldRef?: string; newRef: string }>,
330 |       tags: [] as Array<{ name: string; oldRef?: string; newRef: string }>,
331 |       pruned: [] as string[]
332 |     };
333 | 
334 |     // Parse fetch output
335 |     result.stdout.split('\n').forEach(line => {
336 |       if (line.includes('->')) {
337 |         // Parse branch/tag updates
338 |         const match = line.match(/([a-f0-9]+)\.\.([a-f0-9]+)\s+(\S+)\s+->\s+(\S+)/);
339 |         if (match) {
340 |           const [, oldRef, newRef, localRef, remoteRef] = match;
341 |           if (remoteRef.includes('refs/tags/')) {
342 |             summary.tags.push({
343 |               name: remoteRef.replace('refs/tags/', ''),
344 |               oldRef,
345 |               newRef
346 |             });
347 |           } else {
348 |             summary.branches.push({
349 |               name: remoteRef.replace('refs/remotes/', ''),
350 |               oldRef,
351 |               newRef
352 |             });
353 |           }
354 |         }
355 |       } else if (line.includes('[pruned]')) {
356 |         // Parse pruned refs
357 |         const pruneMatch = line.match(/\[pruned\] (.+)/);
358 |         if (pruneMatch) {
359 |           summary.pruned.push(pruneMatch[1]);
360 |         }
361 |       }
362 |     });
363 | 
364 |     return {
365 |       remote: this.options.remote,
366 |       summary: {
367 |         branches: summary.branches.length > 0 ? summary.branches : undefined,
368 |         tags: summary.tags.length > 0 ? summary.tags : undefined,
369 |         pruned: summary.pruned.length > 0 ? summary.pruned : undefined
370 |       },
371 |       raw: result.stdout
372 |     };
373 |   }
374 | 
375 |   protected getCacheConfig() {
376 |     return {
377 |       command: 'fetch',
378 |       stateType: RepoStateType.REMOTE
379 |     };
380 |   }
381 | 
382 |   protected async validateOptions(): Promise<void> {
383 |     if (this.options.remote && !this.options.all) {
384 |       await RepositoryValidator.validateRemoteConfig(
385 |         this.getResolvedPath(),
386 |         this.options.remote,
387 |         this.context.operation
388 |       );
389 |     }
390 | 
391 |     if (this.options.depth !== undefined && this.options.depth <= 0) {
392 |       throw ErrorHandler.handleValidationError(
393 |         new Error('Depth must be a positive number'),
394 |         { operation: this.context.operation }
395 |       );
396 |     }
397 |   }
398 | }
399 | 
```

--------------------------------------------------------------------------------
/src/tool-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   CallToolRequestSchema,
  3 |   ErrorCode,
  4 |   ListToolsRequestSchema,
  5 |   McpError,
  6 | } from '@modelcontextprotocol/sdk/types.js';
  7 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  8 | import { GitOperations } from './git-operations.js';
  9 | import { logger } from './utils/logger.js';
 10 | import { ErrorHandler } from './errors/error-handler.js';
 11 | import { GitMcpError } from './errors/error-types.js';
 12 | import {
 13 |   isInitOptions,
 14 |   isCloneOptions,
 15 |   isAddOptions,
 16 |   isCommitOptions,
 17 |   isPushPullOptions,
 18 |   isBranchOptions,
 19 |   isCheckoutOptions,
 20 |   isTagOptions,
 21 |   isRemoteOptions,
 22 |   isStashOptions,
 23 |   isPathOnly,
 24 |   isBulkActionOptions,
 25 |   BasePathOptions,
 26 | } from './types.js';
 27 | 
 28 | const PATH_DESCRIPTION = `MUST be an absolute path (e.g., /Users/username/projects/my-repo)`;
 29 | const FILE_PATH_DESCRIPTION = `MUST be an absolute path (e.g., /Users/username/projects/my-repo/src/file.js)`;
 30 | 
 31 | export class ToolHandler {
 32 |   private static readonly TOOL_PREFIX = 'git_mcp_server';
 33 | 
 34 |   constructor(private server: Server) {
 35 |     this.setupHandlers();
 36 |   }
 37 | 
 38 |   private getOperationName(toolName: string): string {
 39 |     return `${ToolHandler.TOOL_PREFIX}.${toolName}`;
 40 |   }
 41 | 
 42 |   private validateArguments<T extends BasePathOptions>(operation: string, args: unknown, validator: (obj: any) => obj is T): T {
 43 |     if (!args || !validator(args)) {
 44 |       throw ErrorHandler.handleValidationError(
 45 |         new Error(`Invalid arguments for operation: ${operation}`),
 46 |         { 
 47 |           operation,
 48 |           details: { args }
 49 |         }
 50 |       );
 51 |     }
 52 | 
 53 |     // If path is not provided, use default path from environment
 54 |     if (!args.path && process.env.GIT_DEFAULT_PATH) {
 55 |       args.path = process.env.GIT_DEFAULT_PATH;
 56 |       logger.info(operation, 'Using default git path', args.path);
 57 |     }
 58 | 
 59 |     return args;
 60 |   }
 61 | 
 62 |   private setupHandlers(): void {
 63 |     this.setupToolDefinitions();
 64 |     this.setupToolExecutor();
 65 |   }
 66 | 
 67 |   private setupToolDefinitions(): void {
 68 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 69 |       tools: [
 70 |         {
 71 |           name: 'init',
 72 |           description: 'Initialize a new Git repository',
 73 |           inputSchema: {
 74 |             type: 'object',
 75 |             properties: {
 76 |               path: {
 77 |                 type: 'string',
 78 |                 description: `Path to initialize the repository in. ${PATH_DESCRIPTION}`,
 79 |               },
 80 |             },
 81 |             required: [],
 82 |           },
 83 |         },
 84 |         {
 85 |           name: 'clone',
 86 |           description: 'Clone a repository',
 87 |           inputSchema: {
 88 |             type: 'object',
 89 |             properties: {
 90 |               url: {
 91 |                 type: 'string',
 92 |                 description: 'URL of the repository to clone',
 93 |               },
 94 |               path: {
 95 |                 type: 'string',
 96 |                 description: `Path to clone into. ${PATH_DESCRIPTION}`,
 97 |               },
 98 |             },
 99 |             required: ['url'],
100 |           },
101 |         },
102 |         {
103 |           name: 'status',
104 |           description: 'Get repository status',
105 |           inputSchema: {
106 |             type: 'object',
107 |             properties: {
108 |               path: {
109 |                 type: 'string',
110 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
111 |               },
112 |             },
113 |             required: [],
114 |           },
115 |         },
116 |         {
117 |           name: 'add',
118 |           description: 'Stage files',
119 |           inputSchema: {
120 |             type: 'object',
121 |             properties: {
122 |               path: {
123 |                 type: 'string',
124 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
125 |               },
126 |               files: {
127 |                 type: 'array',
128 |                 items: {
129 |                   type: 'string',
130 |                   description: FILE_PATH_DESCRIPTION,
131 |                 },
132 |                 description: 'Files to stage',
133 |               },
134 |             },
135 |             required: ['files'],
136 |           },
137 |         },
138 |         {
139 |           name: 'commit',
140 |           description: 'Create a commit',
141 |           inputSchema: {
142 |             type: 'object',
143 |             properties: {
144 |               path: {
145 |                 type: 'string',
146 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
147 |               },
148 |               message: {
149 |                 type: 'string',
150 |                 description: 'Commit message',
151 |               },
152 |             },
153 |             required: ['message'],
154 |           },
155 |         },
156 |         {
157 |           name: 'push',
158 |           description: 'Push commits to remote',
159 |           inputSchema: {
160 |             type: 'object',
161 |             properties: {
162 |               path: {
163 |                 type: 'string',
164 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
165 |               },
166 |               remote: {
167 |                 type: 'string',
168 |                 description: 'Remote name',
169 |                 default: 'origin',
170 |               },
171 |               branch: {
172 |                 type: 'string',
173 |                 description: 'Branch name',
174 |               },
175 |               force: {
176 |                 type: 'boolean',
177 |                 description: 'Force push changes',
178 |                 default: false
179 |               },
180 |               noVerify: {
181 |                 type: 'boolean',
182 |                 description: 'Skip pre-push hooks',
183 |                 default: false
184 |               },
185 |               tags: {
186 |                 type: 'boolean',
187 |                 description: 'Push all tags',
188 |                 default: false
189 |               }
190 |             },
191 |             required: ['branch'],
192 |           },
193 |         },
194 |         {
195 |           name: 'pull',
196 |           description: 'Pull changes from remote',
197 |           inputSchema: {
198 |             type: 'object',
199 |             properties: {
200 |               path: {
201 |                 type: 'string',
202 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
203 |               },
204 |               remote: {
205 |                 type: 'string',
206 |                 description: 'Remote name',
207 |                 default: 'origin',
208 |               },
209 |               branch: {
210 |                 type: 'string',
211 |                 description: 'Branch name',
212 |               },
213 |             },
214 |             required: ['branch'],
215 |           },
216 |         },
217 |         {
218 |           name: 'branch_list',
219 |           description: 'List all branches',
220 |           inputSchema: {
221 |             type: 'object',
222 |             properties: {
223 |               path: {
224 |                 type: 'string',
225 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
226 |               },
227 |             },
228 |             required: [],
229 |           },
230 |         },
231 |         {
232 |           name: 'branch_create',
233 |           description: 'Create a new branch',
234 |           inputSchema: {
235 |             type: 'object',
236 |             properties: {
237 |               path: {
238 |                 type: 'string',
239 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
240 |               },
241 |               name: {
242 |                 type: 'string',
243 |                 description: 'Branch name',
244 |               },
245 |               force: {
246 |                 type: 'boolean',
247 |                 description: 'Force create branch even if it exists',
248 |                 default: false
249 |               },
250 |               track: {
251 |                 type: 'boolean',
252 |                 description: 'Set up tracking mode',
253 |                 default: true
254 |               },
255 |               setUpstream: {
256 |                 type: 'boolean',
257 |                 description: 'Set upstream for push/pull',
258 |                 default: false
259 |               }
260 |             },
261 |             required: ['name'],
262 |           },
263 |         },
264 |         {
265 |           name: 'branch_delete',
266 |           description: 'Delete a branch',
267 |           inputSchema: {
268 |             type: 'object',
269 |             properties: {
270 |               path: {
271 |                 type: 'string',
272 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
273 |               },
274 |               name: {
275 |                 type: 'string',
276 |                 description: 'Branch name',
277 |               },
278 |             },
279 |             required: ['name'],
280 |           },
281 |         },
282 |         {
283 |           name: 'checkout',
284 |           description: 'Switch branches or restore working tree files',
285 |           inputSchema: {
286 |             type: 'object',
287 |             properties: {
288 |               path: {
289 |                 type: 'string',
290 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
291 |               },
292 |               target: {
293 |                 type: 'string',
294 |                 description: 'Branch name, commit hash, or file path',
295 |               },
296 |             },
297 |             required: ['target'],
298 |           },
299 |         },
300 |         {
301 |           name: 'tag_list',
302 |           description: 'List tags',
303 |           inputSchema: {
304 |             type: 'object',
305 |             properties: {
306 |               path: {
307 |                 type: 'string',
308 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
309 |               },
310 |             },
311 |             required: [],
312 |           },
313 |         },
314 |         {
315 |           name: 'tag_create',
316 |           description: 'Create a tag',
317 |           inputSchema: {
318 |             type: 'object',
319 |             properties: {
320 |               path: {
321 |                 type: 'string',
322 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
323 |               },
324 |               name: {
325 |                 type: 'string',
326 |                 description: 'Tag name',
327 |               },
328 |               message: {
329 |                 type: 'string',
330 |                 description: 'Tag message',
331 |               },
332 |               force: {
333 |                 type: 'boolean',
334 |                 description: 'Force create tag even if it exists',
335 |                 default: false
336 |               },
337 |               annotated: {
338 |                 type: 'boolean',
339 |                 description: 'Create an annotated tag',
340 |                 default: true
341 |               },
342 |               sign: {
343 |                 type: 'boolean',
344 |                 description: 'Create a signed tag',
345 |                 default: false
346 |               }
347 |             },
348 |             required: ['name'],
349 |           },
350 |         },
351 |         {
352 |           name: 'tag_delete',
353 |           description: 'Delete a tag',
354 |           inputSchema: {
355 |             type: 'object',
356 |             properties: {
357 |               path: {
358 |                 type: 'string',
359 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
360 |               },
361 |               name: {
362 |                 type: 'string',
363 |                 description: 'Tag name',
364 |               },
365 |             },
366 |             required: ['name'],
367 |           },
368 |         },
369 |         {
370 |           name: 'remote_list',
371 |           description: 'List remotes',
372 |           inputSchema: {
373 |             type: 'object',
374 |             properties: {
375 |               path: {
376 |                 type: 'string',
377 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
378 |               },
379 |             },
380 |             required: [],
381 |           },
382 |         },
383 |         {
384 |           name: 'remote_add',
385 |           description: 'Add a remote',
386 |           inputSchema: {
387 |             type: 'object',
388 |             properties: {
389 |               path: {
390 |                 type: 'string',
391 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
392 |               },
393 |               name: {
394 |                 type: 'string',
395 |                 description: 'Remote name',
396 |               },
397 |               url: {
398 |                 type: 'string',
399 |                 description: 'Remote URL',
400 |               },
401 |             },
402 |             required: ['name', 'url'],
403 |           },
404 |         },
405 |         {
406 |           name: 'remote_remove',
407 |           description: 'Remove a remote',
408 |           inputSchema: {
409 |             type: 'object',
410 |             properties: {
411 |               path: {
412 |                 type: 'string',
413 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
414 |               },
415 |               name: {
416 |                 type: 'string',
417 |                 description: 'Remote name',
418 |               },
419 |             },
420 |             required: ['name'],
421 |           },
422 |         },
423 |         {
424 |           name: 'stash_list',
425 |           description: 'List stashes',
426 |           inputSchema: {
427 |             type: 'object',
428 |             properties: {
429 |               path: {
430 |                 type: 'string',
431 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
432 |               },
433 |             },
434 |             required: [],
435 |           },
436 |         },
437 |         {
438 |           name: 'stash_save',
439 |           description: 'Save changes to stash',
440 |           inputSchema: {
441 |             type: 'object',
442 |             properties: {
443 |               path: {
444 |                 type: 'string',
445 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
446 |               },
447 |               message: {
448 |                 type: 'string',
449 |                 description: 'Stash message',
450 |               },
451 |               includeUntracked: {
452 |                 type: 'boolean',
453 |                 description: 'Include untracked files',
454 |                 default: false
455 |               },
456 |               keepIndex: {
457 |                 type: 'boolean',
458 |                 description: 'Keep staged changes',
459 |                 default: false
460 |               },
461 |               all: {
462 |                 type: 'boolean',
463 |                 description: 'Include ignored files',
464 |                 default: false
465 |               }
466 |             },
467 |             required: [],
468 |           },
469 |         },
470 |         {
471 |           name: 'stash_pop',
472 |           description: 'Apply and remove a stash',
473 |           inputSchema: {
474 |             type: 'object',
475 |             properties: {
476 |               path: {
477 |                 type: 'string',
478 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
479 |               },
480 |               index: {
481 |                 type: 'number',
482 |                 description: 'Stash index',
483 |                 default: 0,
484 |               },
485 |             },
486 |             required: [],
487 |           },
488 |         },
489 |         // New bulk action tool
490 |         {
491 |           name: 'bulk_action',
492 |           description: 'Execute multiple Git operations in sequence. This is the preferred way to execute multiple operations.',
493 |           inputSchema: {
494 |             type: 'object',
495 |             properties: {
496 |               path: {
497 |                 type: 'string',
498 |                 description: `Path to repository. ${PATH_DESCRIPTION}`,
499 |               },
500 |               actions: {
501 |                 type: 'array',
502 |                 description: 'Array of Git operations to execute in sequence',
503 |                 items: {
504 |                   type: 'object',
505 |                   oneOf: [
506 |                     {
507 |                       type: 'object',
508 |                       properties: {
509 |                         type: { const: 'stage' },
510 |                         files: {
511 |                           type: 'array',
512 |                           items: {
513 |                             type: 'string',
514 |                             description: FILE_PATH_DESCRIPTION,
515 |                           },
516 |                           description: 'Files to stage. If not provided, stages all changes.',
517 |                         },
518 |                       },
519 |                       required: ['type'],
520 |                     },
521 |                     {
522 |                       type: 'object',
523 |                       properties: {
524 |                         type: { const: 'commit' },
525 |                         message: {
526 |                           type: 'string',
527 |                           description: 'Commit message',
528 |                         },
529 |                       },
530 |                       required: ['type', 'message'],
531 |                     },
532 |                     {
533 |                       type: 'object',
534 |                       properties: {
535 |                         type: { const: 'push' },
536 |                         remote: {
537 |                           type: 'string',
538 |                           description: 'Remote name',
539 |                           default: 'origin',
540 |                         },
541 |                         branch: {
542 |                           type: 'string',
543 |                           description: 'Branch name',
544 |                         },
545 |                       },
546 |                       required: ['type', 'branch'],
547 |                     },
548 |                   ],
549 |                 },
550 |                 minItems: 1,
551 |               },
552 |             },
553 |             required: ['actions'],
554 |           },
555 |         },
556 |       ],
557 |     }));
558 |   }
559 | 
560 |   private setupToolExecutor(): void {
561 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
562 |       const operation = this.getOperationName(request.params.name);
563 |       const args = request.params.arguments;
564 |       const context = { operation, path: args?.path as string | undefined };
565 | 
566 |       try {
567 |         switch (request.params.name) {
568 |           case 'init': {
569 |             const validArgs = this.validateArguments(operation, args, isInitOptions);
570 |             return await GitOperations.init(validArgs, context);
571 |           }
572 | 
573 |           case 'clone': {
574 |             const validArgs = this.validateArguments(operation, args, isCloneOptions);
575 |             return await GitOperations.clone(validArgs, context);
576 |           }
577 | 
578 |           case 'status': {
579 |             const validArgs = this.validateArguments(operation, args, isPathOnly);
580 |             return await GitOperations.status(validArgs, context);
581 |           }
582 | 
583 |           case 'add': {
584 |             const validArgs = this.validateArguments(operation, args, isAddOptions);
585 |             return await GitOperations.add(validArgs, context);
586 |           }
587 | 
588 |           case 'commit': {
589 |             const validArgs = this.validateArguments(operation, args, isCommitOptions);
590 |             return await GitOperations.commit(validArgs, context);
591 |           }
592 | 
593 |           case 'push': {
594 |             const validArgs = this.validateArguments(operation, args, isPushPullOptions);
595 |             return await GitOperations.push(validArgs, context);
596 |           }
597 | 
598 |           case 'pull': {
599 |             const validArgs = this.validateArguments(operation, args, isPushPullOptions);
600 |             return await GitOperations.pull(validArgs, context);
601 |           }
602 | 
603 |           case 'branch_list': {
604 |             const validArgs = this.validateArguments(operation, args, isPathOnly);
605 |             return await GitOperations.branchList(validArgs, context);
606 |           }
607 | 
608 |           case 'branch_create': {
609 |             const validArgs = this.validateArguments(operation, args, isBranchOptions);
610 |             return await GitOperations.branchCreate(validArgs, context);
611 |           }
612 | 
613 |           case 'branch_delete': {
614 |             const validArgs = this.validateArguments(operation, args, isBranchOptions);
615 |             return await GitOperations.branchDelete(validArgs, context);
616 |           }
617 | 
618 |           case 'checkout': {
619 |             const validArgs = this.validateArguments(operation, args, isCheckoutOptions);
620 |             return await GitOperations.checkout(validArgs, context);
621 |           }
622 | 
623 |           case 'tag_list': {
624 |             const validArgs = this.validateArguments(operation, args, isPathOnly);
625 |             return await GitOperations.tagList(validArgs, context);
626 |           }
627 | 
628 |           case 'tag_create': {
629 |             const validArgs = this.validateArguments(operation, args, isTagOptions);
630 |             return await GitOperations.tagCreate(validArgs, context);
631 |           }
632 | 
633 |           case 'tag_delete': {
634 |             const validArgs = this.validateArguments(operation, args, isTagOptions);
635 |             return await GitOperations.tagDelete(validArgs, context);
636 |           }
637 | 
638 |           case 'remote_list': {
639 |             const validArgs = this.validateArguments(operation, args, isPathOnly);
640 |             return await GitOperations.remoteList(validArgs, context);
641 |           }
642 | 
643 |           case 'remote_add': {
644 |             const validArgs = this.validateArguments(operation, args, isRemoteOptions);
645 |             return await GitOperations.remoteAdd(validArgs, context);
646 |           }
647 | 
648 |           case 'remote_remove': {
649 |             const validArgs = this.validateArguments(operation, args, isRemoteOptions);
650 |             return await GitOperations.remoteRemove(validArgs, context);
651 |           }
652 | 
653 |           case 'stash_list': {
654 |             const validArgs = this.validateArguments(operation, args, isPathOnly);
655 |             return await GitOperations.stashList(validArgs, context);
656 |           }
657 | 
658 |           case 'stash_save': {
659 |             const validArgs = this.validateArguments(operation, args, isStashOptions);
660 |             return await GitOperations.stashSave(validArgs, context);
661 |           }
662 | 
663 |           case 'stash_pop': {
664 |             const validArgs = this.validateArguments(operation, args, isStashOptions);
665 |             return await GitOperations.stashPop(validArgs, context);
666 |           }
667 | 
668 |           case 'bulk_action': {
669 |             const validArgs = this.validateArguments(operation, args, isBulkActionOptions);
670 |             return await GitOperations.executeBulkActions(validArgs, context);
671 |           }
672 | 
673 |           default:
674 |             throw ErrorHandler.handleValidationError(
675 |               new Error(`Unknown tool: ${request.params.name}`),
676 |               { operation }
677 |             );
678 |         }
679 |       } catch (error: unknown) {
680 |         // If it's already a GitMcpError or McpError, rethrow it
681 |         if (error instanceof GitMcpError || error instanceof McpError) {
682 |           throw error;
683 |         }
684 | 
685 |         // Otherwise, wrap it in an appropriate error type
686 |         throw ErrorHandler.handleOperationError(
687 |           error instanceof Error ? error : new Error('Unknown error'),
688 |           {
689 |             operation,
690 |             path: context.path,
691 |             details: { tool: request.params.name }
692 |           }
693 |         );
694 |       }
695 |     });
696 |   }
697 | }
698 | 
```

--------------------------------------------------------------------------------
/src/git-operations.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { CommandExecutor } from './utils/command.js';
  2 | import { PathValidator } from './utils/path.js';
  3 | import { RepositoryValidator } from './utils/repository.js';
  4 | import { logger } from './utils/logger.js';
  5 | import { repositoryCache } from './caching/repository-cache.js';
  6 | import { RepoStateType } from './caching/repository-cache.js';
  7 | import {
  8 |   GitToolResult,
  9 |   GitToolContext,
 10 |   InitOptions,
 11 |   CloneOptions,
 12 |   AddOptions,
 13 |   CommitOptions,
 14 |   PushPullOptions,
 15 |   BranchOptions,
 16 |   CheckoutOptions,
 17 |   TagOptions,
 18 |   RemoteOptions,
 19 |   StashOptions,
 20 |   BasePathOptions,
 21 |   BulkActionOptions,
 22 |   BulkAction,
 23 | } from './types.js';
 24 | import { resolve } from 'path';
 25 | import { existsSync } from 'fs';
 26 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
 27 | import { ErrorHandler } from './errors/error-handler.js';
 28 | import { GitMcpError } from './errors/error-types.js';
 29 | 
 30 | export class GitOperations {
 31 |   private static async executeOperation<T>(
 32 |     operation: string,
 33 |     path: string | undefined,
 34 |     action: () => Promise<T>,
 35 |     options: {
 36 |       useCache?: boolean;
 37 |       stateType?: RepoStateType;
 38 |       command?: string;
 39 |       invalidateCache?: boolean;
 40 |     } = {}
 41 |   ): Promise<T> {
 42 |     try {
 43 |       logger.info(operation, 'Starting git operation', path);
 44 | 
 45 |       let result: T;
 46 |       if (options.useCache && path && options.stateType && options.command) {
 47 |         // Use cache for repository state operations
 48 |         result = await repositoryCache.getState(
 49 |           path,
 50 |           options.stateType,
 51 |           options.command,
 52 |           action
 53 |         );
 54 |       } else if (options.useCache && path && options.command) {
 55 |         // Use cache for command results
 56 |         result = await repositoryCache.getCommandResult(
 57 |           path,
 58 |           options.command,
 59 |           action
 60 |         );
 61 |       } else {
 62 |         // Execute without caching
 63 |         result = await action();
 64 |       }
 65 | 
 66 |       // Invalidate cache if needed
 67 |       if (options.invalidateCache && path) {
 68 |         if (options.stateType) {
 69 |           repositoryCache.invalidateState(path, options.stateType);
 70 |         }
 71 |         if (options.command) {
 72 |           repositoryCache.invalidateCommand(path, options.command);
 73 |         }
 74 |       }
 75 | 
 76 |       logger.info(operation, 'Operation completed successfully', path);
 77 |       return result;
 78 |     } catch (error: unknown) {
 79 |       if (error instanceof GitMcpError) throw error;
 80 |       throw ErrorHandler.handleOperationError(error instanceof Error ? error : new Error('Unknown error'), {
 81 |         operation,
 82 |         path,
 83 |         command: options.command || 'git operation'
 84 |       });
 85 |     }
 86 |   }
 87 | 
 88 |   private static getPath(options: BasePathOptions): string {
 89 |     if (!options.path && !process.env.GIT_DEFAULT_PATH) {
 90 |       throw ErrorHandler.handleValidationError(
 91 |         new Error('Path must be provided when GIT_DEFAULT_PATH is not set'),
 92 |         { operation: 'get_path' }
 93 |       );
 94 |     }
 95 |     return options.path || process.env.GIT_DEFAULT_PATH!;
 96 |   }
 97 | 
 98 |   static async init(options: InitOptions, context: GitToolContext): Promise<GitToolResult> {
 99 |     const path = this.getPath(options);
100 |     return await this.executeOperation(
101 |       context.operation,
102 |       path,
103 |       async () => {
104 |         const pathInfo = PathValidator.validatePath(path, { mustExist: false, allowDirectory: true });
105 |         const result = await CommandExecutor.executeGitCommand(
106 |           'init',
107 |           context.operation,
108 |           pathInfo
109 |         );
110 | 
111 |         return {
112 |           content: [{
113 |             type: 'text',
114 |             text: `Repository initialized successfully\n${CommandExecutor.formatOutput(result)}`
115 |           }]
116 |         };
117 |       },
118 |       {
119 |         command: 'init',
120 |         invalidateCache: true // Invalidate all caches for this repo
121 |       }
122 |     );
123 |   }
124 | 
125 |   static async clone(options: CloneOptions, context: GitToolContext): Promise<GitToolResult> {
126 |     const path = this.getPath(options);
127 |     return await this.executeOperation(
128 |       context.operation,
129 |       path,
130 |       async () => {
131 |         const pathInfo = PathValidator.validatePath(path, { mustExist: false, allowDirectory: true });
132 |         const result = await CommandExecutor.executeGitCommand(
133 |           `clone ${options.url} ${pathInfo}`,
134 |           context.operation
135 |         );
136 | 
137 |         return {
138 |           content: [{
139 |             type: 'text',
140 |             text: `Repository cloned successfully\n${CommandExecutor.formatOutput(result)}`
141 |           }]
142 |         };
143 |       },
144 |       {
145 |         command: 'clone',
146 |         invalidateCache: true // Invalidate all caches for this repo
147 |       }
148 |     );
149 |   }
150 | 
151 |   static async status(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
152 |     const path = this.getPath(options);
153 |     return await this.executeOperation(
154 |       context.operation,
155 |       path,
156 |       async () => {
157 |         const { path: repoPath } = PathValidator.validateGitRepo(path);
158 |         const result = await CommandExecutor.executeGitCommand(
159 |           'status',
160 |           context.operation,
161 |           repoPath
162 |         );
163 | 
164 |         return {
165 |           content: [{
166 |             type: 'text',
167 |             text: CommandExecutor.formatOutput(result)
168 |           }]
169 |         };
170 |       },
171 |       {
172 |         useCache: true,
173 |         stateType: RepoStateType.STATUS,
174 |         command: 'status'
175 |       }
176 |     );
177 |   }
178 | 
179 |   static async add({ path, files }: AddOptions, context: GitToolContext): Promise<GitToolResult> {
180 |     const resolvedPath = this.getPath({ path });
181 |     return await this.executeOperation(
182 |       context.operation,
183 |       resolvedPath,
184 |       async () => {
185 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
186 |         
187 |         // Handle each file individually to avoid path issues
188 |         for (const file of files) {
189 |           await CommandExecutor.executeGitCommand(
190 |             `add "${file}"`,
191 |             context.operation,
192 |             repoPath
193 |           );
194 |         }
195 | 
196 |         return {
197 |           content: [{
198 |             type: 'text',
199 |             text: 'Files staged successfully'
200 |           }]
201 |         };
202 |       },
203 |       {
204 |         command: 'add',
205 |         invalidateCache: true, // Invalidate status cache
206 |         stateType: RepoStateType.STATUS
207 |       }
208 |     );
209 |   }
210 | 
211 |   static async commit({ path, message }: CommitOptions, context: GitToolContext): Promise<GitToolResult> {
212 |     const resolvedPath = this.getPath({ path });
213 |     return await this.executeOperation(
214 |       context.operation,
215 |       resolvedPath,
216 |       async () => {
217 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
218 |         
219 |         // Verify there are staged changes
220 |         const statusResult = await CommandExecutor.executeGitCommand(
221 |           'status --porcelain',
222 |           context.operation,
223 |           repoPath
224 |         );
225 |         
226 |         if (!statusResult.stdout.trim()) {
227 |           return {
228 |             content: [{
229 |               type: 'text',
230 |               text: 'No changes to commit'
231 |             }],
232 |             isError: true
233 |           };
234 |         }
235 | 
236 |         const result = await CommandExecutor.executeGitCommand(
237 |           `commit -m "${message}"`,
238 |           context.operation,
239 |           repoPath
240 |         );
241 | 
242 |         return {
243 |           content: [{
244 |             type: 'text',
245 |             text: `Changes committed successfully\n${CommandExecutor.formatOutput(result)}`
246 |           }]
247 |         };
248 |       },
249 |       {
250 |         command: 'commit',
251 |         invalidateCache: true, // Invalidate status and branch caches
252 |         stateType: RepoStateType.STATUS
253 |       }
254 |     );
255 |   }
256 | 
257 |   static async push({ path, remote = 'origin', branch, force, noVerify, tags }: PushPullOptions, context: GitToolContext): Promise<GitToolResult> {
258 |     const resolvedPath = this.getPath({ path });
259 |     return await this.executeOperation(
260 |       context.operation,
261 |       resolvedPath,
262 |       async () => {
263 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
264 |         await RepositoryValidator.validateRemoteConfig(repoPath, remote, context.operation);
265 |         await RepositoryValidator.validateBranchExists(repoPath, branch, context.operation);
266 |         
267 |         const result = await CommandExecutor.executeGitCommand(
268 |           `push ${remote} ${branch}${force ? ' --force' : ''}${noVerify ? ' --no-verify' : ''}${tags ? ' --tags' : ''}`,
269 |           context.operation,
270 |           repoPath
271 |         );
272 | 
273 |         return {
274 |           content: [{
275 |             type: 'text',
276 |             text: `Changes pushed successfully\n${CommandExecutor.formatOutput(result)}`
277 |           }]
278 |         };
279 |       },
280 |       {
281 |         command: 'push',
282 |         invalidateCache: true, // Invalidate remote cache
283 |         stateType: RepoStateType.REMOTE
284 |       }
285 |     );
286 |   }
287 | 
288 |   static async executeBulkActions(options: BulkActionOptions, context: GitToolContext): Promise<GitToolResult> {
289 |     const resolvedPath = this.getPath(options);
290 |     return await this.executeOperation(
291 |       context.operation,
292 |       resolvedPath,
293 |       async () => {
294 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
295 |         const results: string[] = [];
296 | 
297 |         for (const action of options.actions) {
298 |           try {
299 |             switch (action.type) {
300 |               case 'stage': {
301 |                 const files = action.files || ['.'];
302 |                 const addResult = await this.add({ path: repoPath, files }, context);
303 |                 results.push(addResult.content[0].text);
304 |                 break;
305 |               }
306 |               case 'commit': {
307 |                 const commitResult = await this.commit({ path: repoPath, message: action.message }, context);
308 |                 results.push(commitResult.content[0].text);
309 |                 break;
310 |               }
311 |               case 'push': {
312 |                 const pushResult = await this.push({ 
313 |                   path: repoPath, 
314 |                   remote: action.remote, 
315 |                   branch: action.branch 
316 |                 }, context);
317 |                 results.push(pushResult.content[0].text);
318 |                 break;
319 |               }
320 |             }
321 |           } catch (error: unknown) {
322 |             const errorMessage = error instanceof Error ? error.message : 'Unknown error';
323 |             results.push(`Failed to execute ${action.type}: ${errorMessage}`);
324 |             if (error instanceof Error) {
325 |               logger.error(context.operation, `Bulk action ${action.type} failed`, repoPath, error);
326 |             }
327 |           }
328 |         }
329 | 
330 |         return {
331 |           content: [{
332 |             type: 'text',
333 |             text: results.join('\n\n')
334 |           }]
335 |         };
336 |       },
337 |       {
338 |         command: 'bulk_action',
339 |         invalidateCache: true // Invalidate all caches
340 |       }
341 |     );
342 |   }
343 | 
344 |   static async pull({ path, remote = 'origin', branch }: PushPullOptions, context: GitToolContext): Promise<GitToolResult> {
345 |     const resolvedPath = this.getPath({ path });
346 |     return await this.executeOperation(
347 |       context.operation,
348 |       resolvedPath,
349 |       async () => {
350 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
351 |         await RepositoryValidator.validateRemoteConfig(repoPath, remote, context.operation);
352 |         
353 |         const result = await CommandExecutor.executeGitCommand(
354 |           `pull ${remote} ${branch}`,
355 |           context.operation,
356 |           repoPath
357 |         );
358 | 
359 |         return {
360 |           content: [{
361 |             type: 'text',
362 |             text: `Changes pulled successfully\n${CommandExecutor.formatOutput(result)}`
363 |           }]
364 |         };
365 |       },
366 |       {
367 |         command: 'pull',
368 |         invalidateCache: true // Invalidate all caches
369 |       }
370 |     );
371 |   }
372 | 
373 |   static async branchList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
374 |     const path = this.getPath(options);
375 |     return await this.executeOperation(
376 |       context.operation,
377 |       path,
378 |       async () => {
379 |         const { path: repoPath } = PathValidator.validateGitRepo(path);
380 |         const result = await CommandExecutor.executeGitCommand(
381 |           'branch -a',
382 |           context.operation,
383 |           repoPath
384 |         );
385 | 
386 |         const output = result.stdout.trim();
387 |         return {
388 |           content: [{
389 |             type: 'text',
390 |             text: output || 'No branches found'
391 |           }]
392 |         };
393 |       },
394 |       {
395 |         useCache: true,
396 |         stateType: RepoStateType.BRANCH,
397 |         command: 'branch -a'
398 |       }
399 |     );
400 |   }
401 | 
402 |   static async branchCreate({ path, name, force, track, setUpstream }: BranchOptions, context: GitToolContext): Promise<GitToolResult> {
403 |     const resolvedPath = this.getPath({ path });
404 |     return await this.executeOperation(
405 |       context.operation,
406 |       resolvedPath,
407 |       async () => {
408 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
409 |         PathValidator.validateBranchName(name);
410 |         
411 |         const result = await CommandExecutor.executeGitCommand(
412 |           `checkout -b ${name}${force ? ' --force' : ''}${track ? ' --track' : ' --no-track'}${setUpstream ? ' --set-upstream' : ''}`,
413 |           context.operation,
414 |           repoPath
415 |         );
416 | 
417 |         return {
418 |           content: [{
419 |             type: 'text',
420 |             text: `Branch '${name}' created successfully\n${CommandExecutor.formatOutput(result)}`
421 |           }]
422 |         };
423 |       },
424 |       {
425 |         command: 'branch_create',
426 |         invalidateCache: true, // Invalidate branch cache
427 |         stateType: RepoStateType.BRANCH
428 |       }
429 |     );
430 |   }
431 | 
432 |   static async branchDelete({ path, name }: BranchOptions, context: GitToolContext): Promise<GitToolResult> {
433 |     const resolvedPath = this.getPath({ path });
434 |     return await this.executeOperation(
435 |       context.operation,
436 |       resolvedPath,
437 |       async () => {
438 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
439 |         PathValidator.validateBranchName(name);
440 |         await RepositoryValidator.validateBranchExists(repoPath, name, context.operation);
441 |         
442 |         const currentBranch = await RepositoryValidator.getCurrentBranch(repoPath, context.operation);
443 |         if (currentBranch === name) {
444 |           throw ErrorHandler.handleValidationError(
445 |             new Error(`Cannot delete the currently checked out branch: ${name}`),
446 |             { operation: context.operation, path: repoPath }
447 |           );
448 |         }
449 |         
450 |         const result = await CommandExecutor.executeGitCommand(
451 |           `branch -D ${name}`,
452 |           context.operation,
453 |           repoPath
454 |         );
455 | 
456 |         return {
457 |           content: [{
458 |             type: 'text',
459 |             text: `Branch '${name}' deleted successfully\n${CommandExecutor.formatOutput(result)}`
460 |           }]
461 |         };
462 |       },
463 |       {
464 |         command: 'branch_delete',
465 |         invalidateCache: true, // Invalidate branch cache
466 |         stateType: RepoStateType.BRANCH
467 |       }
468 |     );
469 |   }
470 | 
471 |   static async checkout({ path, target }: CheckoutOptions, context: GitToolContext): Promise<GitToolResult> {
472 |     const resolvedPath = this.getPath({ path });
473 |     return await this.executeOperation(
474 |       context.operation,
475 |       resolvedPath,
476 |       async () => {
477 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
478 |         await RepositoryValidator.ensureClean(repoPath, context.operation);
479 |         
480 |         const result = await CommandExecutor.executeGitCommand(
481 |           `checkout ${target}`,
482 |           context.operation,
483 |           repoPath
484 |         );
485 | 
486 |         return {
487 |           content: [{
488 |             type: 'text',
489 |             text: `Switched to '${target}' successfully\n${CommandExecutor.formatOutput(result)}`
490 |           }]
491 |         };
492 |       },
493 |       {
494 |         command: 'checkout',
495 |         invalidateCache: true, // Invalidate branch and status caches
496 |         stateType: RepoStateType.BRANCH
497 |       }
498 |     );
499 |   }
500 | 
501 |   static async tagList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
502 |     const path = this.getPath(options);
503 |     return await this.executeOperation(
504 |       context.operation,
505 |       path,
506 |       async () => {
507 |         const { path: repoPath } = PathValidator.validateGitRepo(path);
508 |         const result = await CommandExecutor.executeGitCommand(
509 |           'tag -l',
510 |           context.operation,
511 |           repoPath
512 |         );
513 | 
514 |         const output = result.stdout.trim();
515 |         return {
516 |           content: [{
517 |             type: 'text',
518 |             text: output || 'No tags found'
519 |           }]
520 |         };
521 |       },
522 |       {
523 |         useCache: true,
524 |         stateType: RepoStateType.TAG,
525 |         command: 'tag -l'
526 |       }
527 |     );
528 |   }
529 | 
530 |   static async tagCreate({ path, name, message }: TagOptions, context: GitToolContext): Promise<GitToolResult> {
531 |     const resolvedPath = this.getPath({ path });
532 |     return await this.executeOperation(
533 |       context.operation,
534 |       resolvedPath,
535 |       async () => {
536 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
537 |         PathValidator.validateTagName(name);
538 |         
539 |         let command = `tag ${name}`;
540 |         if (typeof message === 'string' && message.length > 0) {
541 |           command = `tag -a ${name} -m "${message}"`;
542 |         }
543 | 
544 |         const result = await CommandExecutor.executeGitCommand(
545 |           command,
546 |           context.operation,
547 |           repoPath
548 |         );
549 | 
550 |         return {
551 |           content: [{
552 |             type: 'text',
553 |             text: `Tag '${name}' created successfully\n${CommandExecutor.formatOutput(result)}`
554 |           }]
555 |         };
556 |       },
557 |       {
558 |         command: 'tag_create',
559 |         invalidateCache: true, // Invalidate tag cache
560 |         stateType: RepoStateType.TAG
561 |       }
562 |     );
563 |   }
564 | 
565 |   static async tagDelete({ path, name }: TagOptions, context: GitToolContext): Promise<GitToolResult> {
566 |     const resolvedPath = this.getPath({ path });
567 |     return await this.executeOperation(
568 |       context.operation,
569 |       resolvedPath,
570 |       async () => {
571 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
572 |         PathValidator.validateTagName(name);
573 |         await RepositoryValidator.validateTagExists(repoPath, name, context.operation);
574 |         
575 |         const result = await CommandExecutor.executeGitCommand(
576 |           `tag -d ${name}`,
577 |           context.operation,
578 |           repoPath
579 |         );
580 | 
581 |         return {
582 |           content: [{
583 |             type: 'text',
584 |             text: `Tag '${name}' deleted successfully\n${CommandExecutor.formatOutput(result)}`
585 |           }]
586 |         };
587 |       },
588 |       {
589 |         command: 'tag_delete',
590 |         invalidateCache: true, // Invalidate tag cache
591 |         stateType: RepoStateType.TAG
592 |       }
593 |     );
594 |   }
595 | 
596 |   static async remoteList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
597 |     const path = this.getPath(options);
598 |     return await this.executeOperation(
599 |       context.operation,
600 |       path,
601 |       async () => {
602 |         const { path: repoPath } = PathValidator.validateGitRepo(path);
603 |         const result = await CommandExecutor.executeGitCommand(
604 |           'remote -v',
605 |           context.operation,
606 |           repoPath
607 |         );
608 | 
609 |         const output = result.stdout.trim();
610 |         return {
611 |           content: [{
612 |             type: 'text',
613 |             text: output || 'No remotes configured'
614 |           }]
615 |         };
616 |       },
617 |       {
618 |         useCache: true,
619 |         stateType: RepoStateType.REMOTE,
620 |         command: 'remote -v'
621 |       }
622 |     );
623 |   }
624 | 
625 |   static async remoteAdd({ path, name, url }: RemoteOptions, context: GitToolContext): Promise<GitToolResult> {
626 |     const resolvedPath = this.getPath({ path });
627 |     return await this.executeOperation(
628 |       context.operation,
629 |       resolvedPath,
630 |       async () => {
631 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
632 |         PathValidator.validateRemoteName(name);
633 |         if (!url) {
634 |           throw ErrorHandler.handleValidationError(
635 |             new Error('URL is required when adding a remote'),
636 |             { operation: context.operation, path: repoPath }
637 |           );
638 |         }
639 |         PathValidator.validateRemoteUrl(url);
640 |         
641 |         const result = await CommandExecutor.executeGitCommand(
642 |           `remote add ${name} ${url}`,
643 |           context.operation,
644 |           repoPath
645 |         );
646 | 
647 |         return {
648 |           content: [{
649 |             type: 'text',
650 |             text: `Remote '${name}' added successfully\n${CommandExecutor.formatOutput(result)}`
651 |           }]
652 |         };
653 |       },
654 |       {
655 |         command: 'remote_add',
656 |         invalidateCache: true, // Invalidate remote cache
657 |         stateType: RepoStateType.REMOTE
658 |       }
659 |     );
660 |   }
661 | 
662 |   static async remoteRemove({ path, name }: RemoteOptions, context: GitToolContext): Promise<GitToolResult> {
663 |     const resolvedPath = this.getPath({ path });
664 |     return await this.executeOperation(
665 |       context.operation,
666 |       resolvedPath,
667 |       async () => {
668 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
669 |         PathValidator.validateRemoteName(name);
670 |         
671 |         const result = await CommandExecutor.executeGitCommand(
672 |           `remote remove ${name}`,
673 |           context.operation,
674 |           repoPath
675 |         );
676 | 
677 |         return {
678 |           content: [{
679 |             type: 'text',
680 |             text: `Remote '${name}' removed successfully\n${CommandExecutor.formatOutput(result)}`
681 |           }]
682 |         };
683 |       },
684 |       {
685 |         command: 'remote_remove',
686 |         invalidateCache: true, // Invalidate remote cache
687 |         stateType: RepoStateType.REMOTE
688 |       }
689 |     );
690 |   }
691 | 
692 |   static async stashList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
693 |     const path = this.getPath(options);
694 |     return await this.executeOperation(
695 |       context.operation,
696 |       path,
697 |       async () => {
698 |         const { path: repoPath } = PathValidator.validateGitRepo(path);
699 |         const result = await CommandExecutor.executeGitCommand(
700 |           'stash list',
701 |           context.operation,
702 |           repoPath
703 |         );
704 | 
705 |         const output = result.stdout.trim();
706 |         return {
707 |           content: [{
708 |             type: 'text',
709 |             text: output || 'No stashes found'
710 |           }]
711 |         };
712 |       },
713 |       {
714 |         useCache: true,
715 |         stateType: RepoStateType.STASH,
716 |         command: 'stash list'
717 |       }
718 |     );
719 |   }
720 | 
721 |   static async stashSave({ path, message, includeUntracked, keepIndex, all }: StashOptions, context: GitToolContext): Promise<GitToolResult> {
722 |     const resolvedPath = this.getPath({ path });
723 |     return await this.executeOperation(
724 |       context.operation,
725 |       resolvedPath,
726 |       async () => {
727 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
728 |         let command = 'stash';
729 |         if (typeof message === 'string' && message.length > 0) {
730 |           command += ` save "${message}"`;
731 |         }
732 |         if (includeUntracked) {
733 |           command += ' --include-untracked';
734 |         }
735 |         if (keepIndex) {
736 |           command += ' --keep-index';
737 |         }
738 |         if (all) {
739 |           command += ' --all';
740 |         }
741 |         const result = await CommandExecutor.executeGitCommand(
742 |           command,
743 |           context.operation,
744 |           repoPath
745 |         );
746 | 
747 |         return {
748 |           content: [{
749 |             type: 'text',
750 |             text: `Changes stashed successfully\n${CommandExecutor.formatOutput(result)}`
751 |           }]
752 |         };
753 |       },
754 |       {
755 |         command: 'stash_save',
756 |         invalidateCache: true, // Invalidate stash and status caches
757 |         stateType: RepoStateType.STASH
758 |       }
759 |     );
760 |   }
761 | 
762 |   static async stashPop({ path, index = 0 }: StashOptions, context: GitToolContext): Promise<GitToolResult> {
763 |     const resolvedPath = this.getPath({ path });
764 |     return await this.executeOperation(
765 |       context.operation,
766 |       resolvedPath,
767 |       async () => {
768 |         const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
769 |         const result = await CommandExecutor.executeGitCommand(
770 |           `stash pop stash@{${index}}`,
771 |           context.operation,
772 |           repoPath
773 |         );
774 | 
775 |         return {
776 |           content: [{
777 |             type: 'text',
778 |             text: `Stash applied successfully\n${CommandExecutor.formatOutput(result)}`
779 |           }]
780 |         };
781 |       },
782 |       {
783 |         command: 'stash_pop',
784 |         invalidateCache: true, // Invalidate stash and status caches
785 |         stateType: RepoStateType.STASH
786 |       }
787 |     );
788 |   }
789 | }
790 | 
```
Page 2/2FirstPrevNextLast