@cyanheads/git-mcp-server 2.1.8 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/mcp-server/server.js +69 -228
  3. package/dist/mcp-server/tools/gitAdd/index.js +2 -4
  4. package/dist/mcp-server/tools/gitAdd/logic.js +40 -116
  5. package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
  8. package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
  9. package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
  10. package/dist/mcp-server/tools/gitCheckout/logic.js +47 -144
  11. package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
  12. package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
  13. package/dist/mcp-server/tools/gitCherryPick/logic.js +47 -173
  14. package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
  15. package/dist/mcp-server/tools/gitClean/index.js +3 -5
  16. package/dist/mcp-server/tools/gitClean/logic.js +45 -154
  17. package/dist/mcp-server/tools/gitClean/registration.js +52 -92
  18. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
  19. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +18 -32
  20. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
  21. package/dist/mcp-server/tools/gitClone/index.js +2 -4
  22. package/dist/mcp-server/tools/gitClone/logic.js +47 -187
  23. package/dist/mcp-server/tools/gitClone/registration.js +51 -42
  24. package/dist/mcp-server/tools/gitCommit/index.js +2 -4
  25. package/dist/mcp-server/tools/gitCommit/logic.js +75 -310
  26. package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
  27. package/dist/mcp-server/tools/gitDiff/index.js +2 -3
  28. package/dist/mcp-server/tools/gitDiff/logic.js +72 -264
  29. package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
  30. package/dist/mcp-server/tools/gitFetch/index.js +2 -3
  31. package/dist/mcp-server/tools/gitFetch/logic.js +38 -136
  32. package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
  33. package/dist/mcp-server/tools/gitInit/index.js +3 -5
  34. package/dist/mcp-server/tools/gitInit/logic.js +40 -162
  35. package/dist/mcp-server/tools/gitInit/registration.js +52 -104
  36. package/dist/mcp-server/tools/gitLog/index.js +2 -3
  37. package/dist/mcp-server/tools/gitLog/logic.js +71 -266
  38. package/dist/mcp-server/tools/gitLog/registration.js +54 -66
  39. package/dist/mcp-server/tools/gitMerge/index.js +3 -5
  40. package/dist/mcp-server/tools/gitMerge/logic.js +45 -191
  41. package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
  42. package/dist/mcp-server/tools/gitPull/index.js +2 -3
  43. package/dist/mcp-server/tools/gitPull/logic.js +39 -156
  44. package/dist/mcp-server/tools/gitPull/registration.js +53 -75
  45. package/dist/mcp-server/tools/gitPush/index.js +2 -3
  46. package/dist/mcp-server/tools/gitPush/logic.js +65 -192
  47. package/dist/mcp-server/tools/gitPush/registration.js +53 -75
  48. package/dist/mcp-server/tools/gitRebase/index.js +3 -5
  49. package/dist/mcp-server/tools/gitRebase/logic.js +59 -207
  50. package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
  51. package/dist/mcp-server/tools/gitRemote/index.js +3 -5
  52. package/dist/mcp-server/tools/gitRemote/logic.js +76 -200
  53. package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
  54. package/dist/mcp-server/tools/gitReset/index.js +2 -3
  55. package/dist/mcp-server/tools/gitReset/logic.js +33 -133
  56. package/dist/mcp-server/tools/gitReset/registration.js +53 -60
  57. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
  58. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +39 -144
  59. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
  60. package/dist/mcp-server/tools/gitShow/index.js +3 -5
  61. package/dist/mcp-server/tools/gitShow/logic.js +28 -133
  62. package/dist/mcp-server/tools/gitShow/registration.js +52 -74
  63. package/dist/mcp-server/tools/gitStash/index.js +3 -5
  64. package/dist/mcp-server/tools/gitStash/logic.js +59 -219
  65. package/dist/mcp-server/tools/gitStash/registration.js +52 -77
  66. package/dist/mcp-server/tools/gitStatus/index.js +2 -4
  67. package/dist/mcp-server/tools/gitStatus/logic.js +79 -236
  68. package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
  69. package/dist/mcp-server/tools/gitTag/index.js +3 -5
  70. package/dist/mcp-server/tools/gitTag/logic.js +57 -198
  71. package/dist/mcp-server/tools/gitTag/registration.js +54 -73
  72. package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
  73. package/dist/mcp-server/tools/gitWorktree/logic.js +102 -328
  74. package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
  75. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
  76. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +25 -43
  77. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
  78. package/dist/mcp-server/transports/httpTransport.js +2 -3
  79. package/package.json +8 -8
@@ -1,50 +1,59 @@
1
- // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from "../../../utils/index.js";
3
- // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from "../../../utils/index.js";
5
- // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
- import { requestContextService } from "../../../utils/index.js";
7
- import { GitCloneInputSchema, gitCloneLogic, } from "./logic.js";
8
- import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
1
+ /**
2
+ * @fileoverview Handles registration and error handling for the git_clone tool.
3
+ * @module src/mcp-server/tools/gitClone/registration
4
+ */
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { gitCloneLogic, GitCloneInputSchema, GitCloneOutputSchema, } from "./logic.js";
9
7
  const TOOL_NAME = "git_clone";
10
8
  const TOOL_DESCRIPTION = "Clones a Git repository from a given URL into a specified absolute directory path. Supports cloning specific branches and setting clone depth.";
11
9
  /**
12
- * Registers the git_clone tool with the MCP server.
13
- *
14
- * @param {McpServer} server - The McpServer instance to register the tool with.
15
- * @returns {Promise<void>}
16
- * @throws {Error} If registration fails.
10
+ * Registers the git_clone tool with the MCP server instance.
11
+ * @param server The MCP server instance.
12
+ * @param getSessionId Function to get the session ID from context.
17
13
  */
18
- export const registerGitCloneTool = async (server) => {
14
+ export const registerGitCloneTool = async (server, getSessionId) => {
19
15
  const operation = "registerGitCloneTool";
20
16
  const context = requestContextService.createRequestContext({ operation });
21
- await ErrorHandler.tryCatch(async () => {
22
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCloneInputSchema.shape, // Provide the Zod schema shape
23
- async (validatedArgs, callContext) => {
24
- const toolOperation = "tool:git_clone";
25
- const requestContext = requestContextService.createRequestContext({
26
- operation: toolOperation,
27
- parentContext: callContext,
28
- });
29
- logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
30
- return await ErrorHandler.tryCatch(async () => {
31
- // Call the core logic function
32
- const cloneResult = await gitCloneLogic(validatedArgs, requestContext);
33
- // Format the result as a JSON string within TextContent
34
- const resultContent = {
35
- type: "text",
36
- text: JSON.stringify(cloneResult, null, 2), // Pretty-print JSON
37
- contentType: "application/json",
38
- };
39
- logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, requestContext);
40
- return { content: [resultContent] };
41
- }, {
42
- operation: toolOperation,
43
- context: requestContext,
44
- input: validatedArgs, // Log sanitized input
45
- errorCode: BaseErrorCode.INTERNAL_ERROR, // Default if unexpected error occurs
46
- });
17
+ server.registerTool(TOOL_NAME, {
18
+ title: "Git Clone",
19
+ description: TOOL_DESCRIPTION,
20
+ inputSchema: GitCloneInputSchema.shape,
21
+ outputSchema: GitCloneOutputSchema.shape,
22
+ annotations: {
23
+ readOnlyHint: false,
24
+ destructiveHint: true, // Creates new files/directories
25
+ idempotentHint: false,
26
+ openWorldHint: true, // Interacts with remote repositories
27
+ },
28
+ }, async (params, callContext) => {
29
+ const handlerContext = requestContextService.createRequestContext({
30
+ toolName: TOOL_NAME,
31
+ parentContext: callContext,
47
32
  });
48
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
49
- }, { operation, context, critical: true }); // Mark registration as critical
33
+ try {
34
+ const result = await gitCloneLogic(params, handlerContext);
35
+ return {
36
+ structuredContent: result,
37
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
38
+ };
39
+ }
40
+ catch (error) {
41
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
42
+ const mcpError = ErrorHandler.handleError(error, {
43
+ operation: `tool:${TOOL_NAME}`,
44
+ context: handlerContext,
45
+ input: params,
46
+ });
47
+ return {
48
+ isError: true,
49
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
50
+ structuredContent: {
51
+ code: mcpError.code,
52
+ message: mcpError.message,
53
+ details: mcpError.details,
54
+ },
55
+ };
56
+ }
57
+ });
58
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
50
59
  };
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitCommit tool.
3
- * Exports the registration function and state accessor initialization function.
3
+ * @module src/mcp-server/tools/gitCommit/index
4
4
  */
5
- export { initializeGitCommitStateAccessors, registerGitCommitTool, } from "./registration.js";
6
- // Export types if needed elsewhere, e.g.:
7
- // export type { GitCommitInput, GitCommitResult } from './logic.js';
5
+ export { registerGitCommitTool } from "./registration.js";
@@ -1,329 +1,94 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_commit tool.
3
+ * @module src/mcp-server/tools/gitCommit/logic
4
+ */
1
5
  import { execFile } from "child_process";
2
6
  import { promisify } from "util";
3
7
  import { z } from "zod";
4
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
5
- // Import utils from barrel (logger from ../utils/internal/logger.js)
6
- import { logger } from "../../../utils/index.js";
7
- // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
8
- import { sanitization } from "../../../utils/index.js";
9
- // Import config to check signing flag
8
+ import { logger, sanitization } from "../../../utils/index.js";
9
+ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
10
10
  import { config } from "../../../config/index.js";
11
11
  const execFileAsync = promisify(execFile);
12
- // Define the input schema for the git_commit tool using Zod
12
+ // 1. DEFINE the Zod input schema.
13
13
  export const GitCommitInputSchema = z.object({
14
- path: z
15
- .string()
16
- .min(1)
17
- .optional()
18
- .default(".")
19
- .describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
20
- message: z
21
- .string()
22
- .min(1)
23
- .describe("Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`"),
24
- author: z
25
- .object({
26
- name: z.string().describe("Author name for the commit"),
27
- email: z.string().email().describe("Author email for the commit"),
28
- })
29
- .optional()
30
- .describe("Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches)."),
31
- allowEmpty: z
32
- .boolean()
33
- .default(false)
34
- .describe("Allow creating empty commits"),
35
- amend: z
36
- .boolean()
37
- .default(false)
38
- .describe("Amend the previous commit instead of creating a new one"),
39
- forceUnsignedOnFailure: z
40
- .boolean()
41
- .default(false)
42
- .describe("If true and signing is enabled but fails, attempt the commit without signing instead of failing."),
43
- filesToStage: z
44
- .array(z.string().min(1))
45
- .optional()
46
- .describe("Optional array of specific file paths (relative to the repository root) to stage automatically before committing. If provided, only these files will be staged."),
14
+ path: z.string().default(".").describe("Path to the Git repository."),
15
+ message: z.string().min(1).describe("Commit message, preferably following Conventional Commits format."),
16
+ author: z.object({ name: z.string(), email: z.string().email() }).optional().describe("Override the commit author."),
17
+ allowEmpty: z.boolean().default(false).describe("Allow creating a commit with no changes."),
18
+ amend: z.boolean().default(false).describe("Amend the previous commit."),
19
+ forceUnsignedOnFailure: z.boolean().default(false).describe("If signing fails, attempt the commit without a signature."),
20
+ filesToStage: z.array(z.string().min(1)).optional().describe("An array of file paths to stage before committing."),
47
21
  });
22
+ // 2. DEFINE the Zod response schema.
23
+ export const GitCommitOutputSchema = z.object({
24
+ success: z.boolean().describe("Indicates if the command was successful."),
25
+ message: z.string().describe("A summary message of the result."),
26
+ commitHash: z.string().optional().describe("The hash of the new commit."),
27
+ committedFiles: z.array(z.string()).optional().describe("A list of files included in the commit."),
28
+ nothingToCommit: z.boolean().optional().describe("True if there were no changes to commit."),
29
+ });
30
+ async function stageFiles(targetPath, files, context) {
31
+ const operation = "stageFilesForCommit";
32
+ logger.debug(`Staging files: ${files.join(", ")}`, { ...context, operation });
33
+ const sanitizedFiles = files.map(file => sanitization.sanitizePath(file, { rootDir: targetPath }).sanitizedPath);
34
+ await execFileAsync("git", ["-C", targetPath, "add", "--", ...sanitizedFiles]);
35
+ }
36
+ async function getCommittedFiles(targetPath, commitHash, context) {
37
+ const { stdout } = await execFileAsync("git", ["-C", targetPath, "show", "--pretty=", "--name-only", commitHash]);
38
+ return stdout.trim().split("\n").filter(Boolean);
39
+ }
48
40
  /**
49
- * Executes the 'git commit' command and returns structured JSON output.
50
- *
51
- * @param {GitCommitInput} input - The validated input object.
52
- * @param {RequestContext} context - The request context for logging and error handling.
53
- * @returns {Promise<GitCommitResult>} A promise that resolves with the structured commit result.
54
- * @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails unexpectedly.
41
+ * 4. IMPLEMENT the core logic function.
42
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
55
43
  */
56
- export async function commitGitChanges(input, context) {
44
+ export async function commitGitChanges(params, context) {
57
45
  const operation = "commitGitChanges";
58
- logger.debug(`Executing ${operation}`, { ...context, input });
59
- let targetPath;
60
- try {
61
- // Resolve the target path
62
- if (input.path && input.path !== ".") {
63
- targetPath = input.path;
64
- logger.debug(`Using provided path: ${targetPath}`, {
65
- ...context,
66
- operation,
67
- });
68
- }
69
- else {
70
- const workingDir = context.getWorkingDirectory();
71
- if (!workingDir) {
72
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
73
- }
74
- targetPath = workingDir;
75
- logger.debug(`Using session working directory: ${targetPath}`, {
76
- ...context,
77
- operation,
78
- sessionId: context.sessionId,
79
- });
80
- }
81
- // Sanitize the resolved path
82
- const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
83
- allowAbsolute: true,
84
- });
85
- logger.debug("Sanitized path", {
86
- ...context,
87
- operation,
88
- sanitizedPathInfo,
89
- });
90
- targetPath = sanitizedPathInfo.sanitizedPath; // Use the sanitized path going forward
46
+ logger.debug(`Executing ${operation}`, { ...context, params });
47
+ const workingDir = context.getWorkingDirectory();
48
+ if (params.path === "." && !workingDir) {
49
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
91
50
  }
92
- catch (error) {
93
- logger.error("Path resolution or sanitization failed", {
94
- ...context,
95
- operation,
96
- error,
97
- });
98
- if (error instanceof McpError) {
99
- throw error;
100
- }
101
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
51
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
52
+ if (params.filesToStage && params.filesToStage.length > 0) {
53
+ await stageFiles(targetPath, params.filesToStage, context);
102
54
  }
55
+ const args = ["-C", targetPath];
56
+ if (params.author)
57
+ args.push("-c", `user.name=${params.author.name}`, "-c", `user.email=${params.author.email}`);
58
+ args.push("commit", "-m", params.message);
59
+ if (params.allowEmpty)
60
+ args.push("--allow-empty");
61
+ if (params.amend)
62
+ args.push("--amend", "--no-edit");
63
+ const attemptCommit = async (withSigning) => {
64
+ const finalArgs = [...args];
65
+ if (withSigning)
66
+ finalArgs.push("-S");
67
+ logger.debug(`Executing command: git ${finalArgs.join(" ")}`, { ...context, operation });
68
+ return await execFileAsync("git", finalArgs);
69
+ };
70
+ let result;
71
+ const shouldSign = config.gitSignCommits;
103
72
  try {
104
- // --- Stage specific files if requested ---
105
- if (input.filesToStage && input.filesToStage.length > 0) {
106
- logger.debug(`Attempting to stage specific files: ${input.filesToStage.join(", ")}`, { ...context, operation });
107
- try {
108
- // Correctly pass targetPath as rootDir in options object
109
- const sanitizedFiles = input.filesToStage.map((file) => sanitization.sanitizePath(file, { rootDir: targetPath })
110
- .sanitizedPath); // Sanitize relative to repo root
111
- const addArgs = ["-C", targetPath, "add", "--", ...sanitizedFiles];
112
- logger.debug(`Executing git add command: git ${addArgs.join(" ")}`, {
113
- ...context,
114
- operation,
115
- });
116
- await execFileAsync("git", addArgs);
117
- logger.info(`Successfully staged specified files: ${sanitizedFiles.join(", ")}`, { ...context, operation });
118
- }
119
- catch (addError) {
120
- logger.error("Failed to stage specified files", {
121
- ...context,
122
- operation,
123
- files: input.filesToStage,
124
- error: addError.message,
125
- stderr: addError.stderr,
126
- });
127
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to stage files before commit: ${addError.stderr || addError.message}`, { context, operation, originalError: addError });
128
- }
129
- }
130
- // --- End staging files ---
131
- // Construct the git commit command using the resolved targetPath
132
- const args = ["-C", targetPath];
133
- if (input.author) {
134
- args.push("-c", `user.name=${input.author.name}`, "-c", `user.email=${input.author.email}`);
135
- }
136
- args.push("commit", "-m", input.message);
137
- if (input.allowEmpty) {
138
- args.push("--allow-empty");
139
- }
140
- if (input.amend) {
141
- args.push("--amend", "--no-edit");
142
- }
143
- // Append signing flag if configured via GIT_SIGN_COMMITS env var
144
- if (config.gitSignCommits) {
145
- args.push("-S"); // Add signing flag (-S)
146
- logger.info("Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.", { ...context, operation });
147
- }
148
- logger.debug(`Executing initial command attempt: git ${args.join(" ")}`, {
149
- ...context,
150
- operation,
151
- });
152
- let stdout;
153
- let stderr;
154
- let commitResult;
155
- try {
156
- // Initial attempt (potentially with -S flag)
157
- const execResult = await execFileAsync("git", args);
158
- stdout = execResult.stdout;
159
- stderr = execResult.stderr;
160
- }
161
- catch (error) {
162
- const initialErrorMessage = error.stderr || error.message || "";
163
- const isSigningError = initialErrorMessage.includes("gpg failed to sign") ||
164
- initialErrorMessage.includes("signing failed");
165
- if (isSigningError && input.forceUnsignedOnFailure) {
166
- logger.warning("Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.", { ...context, operation, initialError: initialErrorMessage });
167
- // Construct command *without* -S flag
168
- const unsignedArgs = args.filter((arg) => arg !== "-S");
169
- logger.debug(`Executing unsigned fallback command: git ${unsignedArgs.join(" ")}`, { ...context, operation });
170
- try {
171
- // Retry commit without signing
172
- const fallbackResult = await execFileAsync("git", unsignedArgs);
173
- stdout = fallbackResult.stdout;
174
- stderr = fallbackResult.stderr;
175
- // Add a note to the status message indicating signing was skipped
176
- commitResult = {
177
- success: true,
178
- statusMessage: `Commit successful (unsigned, signing failed): ${stdout.trim()}`, // Default message, hash parsed below
179
- commitHash: undefined, // Will be parsed below
180
- };
181
- }
182
- catch (fallbackError) {
183
- // If the unsigned commit *also* fails, re-throw that error
184
- logger.error("Unsigned fallback commit attempt also failed.", {
185
- ...context,
186
- operation,
187
- fallbackError: fallbackError.message,
188
- stderr: fallbackError.stderr,
189
- });
190
- throw fallbackError; // Re-throw the error from the unsigned attempt
191
- }
192
- }
193
- else {
194
- // If it wasn't a signing error, or forceUnsignedOnFailure is false, re-throw the original error
195
- throw error;
196
- }
197
- }
198
- // Process result (either from initial attempt or fallback)
199
- // Check stderr first for common non-error messages
200
- if (stderr && !commitResult) {
201
- // Don't overwrite fallback message if stderr also exists
202
- if (stderr.includes("nothing to commit, working tree clean") ||
203
- stderr.includes("no changes added to commit")) {
204
- const msg = stderr.includes("nothing to commit")
205
- ? "Nothing to commit, working tree clean."
206
- : "No changes added to commit.";
207
- logger.info(msg, { ...context, operation, path: targetPath });
208
- // Use statusMessage
209
- return { success: true, statusMessage: msg, nothingToCommit: true };
210
- }
211
- // Log other stderr as warning but continue, as commit might still succeed
212
- logger.warning(`Git commit command produced stderr`, {
213
- ...context,
214
- operation,
215
- stderr,
216
- });
217
- }
218
- // Extract commit hash (more robustly)
219
- let commitHash = undefined;
220
- const hashMatch = stdout.match(/([a-f0-9]{7,40})/); // Look for typical short or long hash
221
- if (hashMatch) {
222
- commitHash = hashMatch[1];
223
- }
224
- else {
225
- // Fallback parsing if needed, or rely on success message
226
- logger.warning("Could not parse commit hash from stdout", {
227
- ...context,
228
- operation,
229
- stdout,
230
- });
231
- }
232
- // Use statusMessage, potentially using the one set during fallback
233
- const finalStatusMsg = commitResult?.statusMessage ||
234
- (commitHash
235
- ? `Commit successful: ${commitHash}`
236
- : `Commit successful (stdout: ${stdout.trim()})`);
237
- let committedFiles = [];
238
- if (commitHash) {
239
- try {
240
- // Get the list of files included in this specific commit
241
- const showArgs = [
242
- "-C",
243
- targetPath,
244
- "show",
245
- "--pretty=",
246
- "--name-only",
247
- commitHash,
248
- ];
249
- logger.debug(`Executing git show command: git ${showArgs.join(" ")}`, {
250
- ...context,
251
- operation,
252
- });
253
- const { stdout: showStdout } = await execFileAsync("git", showArgs);
254
- committedFiles = showStdout.trim().split("\n").filter(Boolean); // Split by newline, remove empty lines
255
- logger.debug(`Retrieved committed files list for ${commitHash}`, {
256
- ...context,
257
- operation,
258
- count: committedFiles.length,
259
- });
260
- }
261
- catch (showError) {
262
- // Log a warning but don't fail the overall operation if we can't get the file list
263
- logger.warning("Failed to retrieve committed files list", {
264
- ...context,
265
- operation,
266
- commitHash,
267
- error: showError.message,
268
- stderr: showError.stderr,
269
- });
270
- }
271
- }
272
- const successMessage = `Commit successful: ${commitHash}`;
273
- logger.info(successMessage, {
274
- ...context,
275
- operation,
276
- path: targetPath,
277
- commitHash,
278
- signed: !commitResult, // Log if it was signed (not fallback)
279
- committedFilesCount: committedFiles.length,
280
- });
281
- return {
282
- success: true,
283
- statusMessage: finalStatusMsg, // Use potentially modified message
284
- commitHash: commitHash,
285
- commitMessage: input.message, // Include the original commit message
286
- committedFiles: committedFiles, // Include the list of files
287
- };
73
+ result = await attemptCommit(shouldSign);
288
74
  }
289
75
  catch (error) {
290
- // This catch block now primarily handles non-signing errors or errors from the fallback attempt
291
- logger.error(`Failed to execute git commit command`, {
292
- ...context,
293
- operation,
294
- path: targetPath,
295
- error: error.message,
296
- stderr: error.stderr,
297
- });
298
- const errorMessage = error.stderr || error.message || "";
299
- // Handle specific error cases first
300
- if (errorMessage.toLowerCase().includes("not a git repository")) {
301
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
302
- }
303
- // Check for pre-commit hook failures before checking for generic conflicts
304
- if (errorMessage.toLowerCase().includes("pre-commit hook") ||
305
- errorMessage.toLowerCase().includes("hook failed")) {
306
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Commit failed due to pre-commit hook failure. Details: ${errorMessage}`, { context, operation, originalError: error });
76
+ const isSigningError = (error.stderr || "").includes("gpg failed to sign");
77
+ if (shouldSign && isSigningError && params.forceUnsignedOnFailure) {
78
+ logger.warning("Commit with signing failed. Retrying without signature.", { ...context, operation });
79
+ result = await attemptCommit(false);
307
80
  }
308
- if (errorMessage.includes("nothing to commit") ||
309
- errorMessage.includes("no changes added to commit")) {
310
- // This might happen if git exits with error despite these messages
311
- const msg = errorMessage.includes("nothing to commit")
312
- ? "Nothing to commit, working tree clean."
313
- : "No changes added to commit.";
314
- logger.info(msg + " (caught as error)", {
315
- ...context,
316
- operation,
317
- path: targetPath,
318
- errorMessage,
319
- });
320
- // Return success=false but indicate the reason using statusMessage
321
- return { success: false, statusMessage: msg, nothingToCommit: true };
322
- }
323
- if (errorMessage.includes("conflicts")) {
324
- throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
81
+ else {
82
+ throw error;
325
83
  }
326
- // Generic internal error for other failures
327
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to commit changes for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
328
84
  }
85
+ const commitHashMatch = result.stdout.match(/([a-f0-9]{7,40})/);
86
+ const commitHash = commitHashMatch ? commitHashMatch[1] : undefined;
87
+ const committedFiles = commitHash ? await getCommittedFiles(targetPath, commitHash, context) : [];
88
+ return {
89
+ success: true,
90
+ message: `Commit successful: ${commitHash}`,
91
+ commitHash,
92
+ committedFiles,
93
+ };
329
94
  }