@cyanheads/git-mcp-server 2.1.8 → 2.2.0

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 +17 -74
  5. package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
  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 -122
  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 +55 -162
  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 +44 -143
  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 +19 -26
  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 +50 -171
  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 +90 -295
  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 +78 -254
  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 +47 -129
  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 +46 -152
  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 +75 -257
  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 +52 -179
  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 +48 -146
  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 +73 -181
  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 +73 -202
  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 +85 -193
  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 +37 -121
  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 +45 -133
  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 +33 -122
  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 +70 -214
  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 +82 -229
  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 +66 -188
  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 +112 -322
  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 +26 -38
  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 { McpError, BaseErrorCode } from "../../../types-global/errors.js";
7
+ import { gitCloneLogic, GitCloneInputSchema, GitCloneOutputSchema, } from "./logic.js";
9
8
  const TOOL_NAME = "git_clone";
10
9
  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
10
  /**
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.
11
+ * Registers the git_clone tool with the MCP server instance.
12
+ * @param server The MCP server instance.
13
+ * @param getSessionId Function to get the session ID from context.
17
14
  */
18
- export const registerGitCloneTool = async (server) => {
15
+ export const registerGitCloneTool = async (server, getSessionId) => {
19
16
  const operation = "registerGitCloneTool";
20
17
  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
- });
18
+ server.registerTool(TOOL_NAME, {
19
+ title: "Git Clone",
20
+ description: TOOL_DESCRIPTION,
21
+ inputSchema: GitCloneInputSchema.shape,
22
+ outputSchema: GitCloneOutputSchema.shape,
23
+ annotations: {
24
+ readOnlyHint: false,
25
+ destructiveHint: true, // Creates new files/directories
26
+ idempotentHint: false,
27
+ openWorldHint: true, // Interacts with remote repositories
28
+ },
29
+ }, async (params, callContext) => {
30
+ const handlerContext = requestContextService.createRequestContext({
31
+ toolName: TOOL_NAME,
32
+ parentContext: callContext,
47
33
  });
48
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
49
- }, { operation, context, critical: true }); // Mark registration as critical
34
+ try {
35
+ const result = await gitCloneLogic(params, handlerContext);
36
+ return {
37
+ structuredContent: result,
38
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
39
+ };
40
+ }
41
+ catch (error) {
42
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
43
+ const handledError = ErrorHandler.handleError(error, {
44
+ operation: `tool:${TOOL_NAME}`,
45
+ context: handlerContext,
46
+ input: params,
47
+ });
48
+ const mcpError = handledError instanceof McpError
49
+ ? handledError
50
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
51
+ return {
52
+ isError: true,
53
+ content: [{ type: "text", text: mcpError.message }],
54
+ structuredContent: mcpError.details,
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,124 @@
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."),
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."),
47
29
  });
30
+ async function stageFiles(targetPath, files, context) {
31
+ const operation = "stageFilesForCommit";
32
+ logger.debug(`Staging files: ${files.join(", ")}`, { ...context, operation });
33
+ try {
34
+ const sanitizedFiles = files.map(file => sanitization.sanitizePath(file, { rootDir: targetPath }).sanitizedPath);
35
+ await execFileAsync("git", ["-C", targetPath, "add", "--", ...sanitizedFiles]);
36
+ }
37
+ catch (error) {
38
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to stage files: ${error.stderr || error.message}`);
39
+ }
40
+ }
41
+ async function getCommittedFiles(targetPath, commitHash, context) {
42
+ try {
43
+ const { stdout } = await execFileAsync("git", ["-C", targetPath, "show", "--pretty=", "--name-only", commitHash]);
44
+ return stdout.trim().split("\n").filter(Boolean);
45
+ }
46
+ catch (error) {
47
+ logger.warning("Failed to retrieve committed files list", { ...context, commitHash, error: error.message });
48
+ return [];
49
+ }
50
+ }
48
51
  /**
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.
52
+ * 4. IMPLEMENT the core logic function.
53
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
55
54
  */
56
- export async function commitGitChanges(input, context) {
55
+ export async function commitGitChanges(params, context) {
57
56
  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
57
+ logger.debug(`Executing ${operation}`, { ...context, params });
58
+ const workingDir = context.getWorkingDirectory();
59
+ if (params.path === "." && !workingDir) {
60
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
91
61
  }
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 });
62
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
63
+ if (params.filesToStage && params.filesToStage.length > 0) {
64
+ await stageFiles(targetPath, params.filesToStage, context);
102
65
  }
66
+ const args = ["-C", targetPath];
67
+ if (params.author)
68
+ args.push("-c", `user.name=${params.author.name}`, "-c", `user.email=${params.author.email}`);
69
+ args.push("commit", "-m", params.message);
70
+ if (params.allowEmpty)
71
+ args.push("--allow-empty");
72
+ if (params.amend)
73
+ args.push("--amend", "--no-edit");
74
+ const attemptCommit = async (withSigning) => {
75
+ const finalArgs = [...args];
76
+ if (withSigning)
77
+ finalArgs.push("-S");
78
+ logger.debug(`Executing command: git ${finalArgs.join(" ")}`, { ...context, operation });
79
+ return await execFileAsync("git", finalArgs);
80
+ };
103
81
  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;
82
+ let result;
83
+ const shouldSign = config.gitSignCommits;
155
84
  try {
156
- // Initial attempt (potentially with -S flag)
157
- const execResult = await execFileAsync("git", args);
158
- stdout = execResult.stdout;
159
- stderr = execResult.stderr;
85
+ result = await attemptCommit(shouldSign);
160
86
  }
161
87
  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
- }
88
+ const isSigningError = (error.stderr || "").includes("gpg failed to sign");
89
+ if (shouldSign && isSigningError && params.forceUnsignedOnFailure) {
90
+ logger.warning("Commit with signing failed. Retrying without signature.", { ...context, operation });
91
+ result = await attemptCommit(false);
192
92
  }
193
93
  else {
194
- // If it wasn't a signing error, or forceUnsignedOnFailure is false, re-throw the original error
195
94
  throw error;
196
95
  }
197
96
  }
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
- });
97
+ const commitHashMatch = result.stdout.match(/([a-f0-9]{7,40})/);
98
+ const commitHash = commitHashMatch ? commitHashMatch[1] : undefined;
99
+ const committedFiles = commitHash ? await getCommittedFiles(targetPath, commitHash, context) : [];
281
100
  return {
282
101
  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
102
+ message: `Commit successful: ${commitHash}`,
103
+ commitHash,
104
+ committedFiles,
287
105
  };
288
106
  }
289
107
  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
108
  const errorMessage = error.stderr || error.message || "";
299
- // Handle specific error cases first
109
+ logger.error(`Failed to execute git commit command`, { ...context, operation, errorMessage });
300
110
  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 });
111
+ throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
302
112
  }
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 });
307
- }
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 };
113
+ if (errorMessage.includes("nothing to commit")) {
114
+ return { success: true, message: "Nothing to commit, working tree clean.", nothingToCommit: true };
322
115
  }
323
116
  if (errorMessage.includes("conflicts")) {
324
- throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
117
+ throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts.`);
118
+ }
119
+ if (errorMessage.includes("pre-commit hook")) {
120
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Commit failed due to pre-commit hook failure.`);
325
121
  }
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 });
122
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git commit failed: ${errorMessage}`);
328
123
  }
329
124
  }