@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.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +44 -143
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +50 -171
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +46 -152
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +75 -257
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +48 -146
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +73 -181
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +37 -121
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +33 -122
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +70 -214
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +66 -188
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +8 -8
|
@@ -1,50 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import { GitCloneInputSchema,
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
*
|
|
3
|
+
* @module src/mcp-server/tools/gitCommit/index
|
|
4
4
|
*/
|
|
5
|
-
export {
|
|
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 {
|
|
5
|
-
|
|
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
|
-
//
|
|
12
|
+
// 1. DEFINE the Zod input schema.
|
|
13
13
|
export const GitCommitInputSchema = z.object({
|
|
14
|
-
path: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
*
|
|
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(
|
|
55
|
+
export async function commitGitChanges(params, context) {
|
|
57
56
|
const operation = "commitGitChanges";
|
|
58
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
284
|
-
commitHash
|
|
285
|
-
|
|
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
|
-
|
|
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}
|
|
111
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
302
112
|
}
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|