@cyanheads/git-mcp-server 2.1.0 → 2.1.2
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 +8 -11
- package/dist/config/index.js +7 -7
- package/dist/index.js +35 -21
- package/dist/mcp-server/server.js +72 -56
- package/dist/mcp-server/tools/gitAdd/index.js +1 -1
- package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
- package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
- package/dist/mcp-server/tools/gitBranch/index.js +1 -1
- package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
- package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
- package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
- package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
- package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
- package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
- package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
- package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
- package/dist/mcp-server/tools/gitClean/index.js +1 -1
- package/dist/mcp-server/tools/gitClean/logic.js +93 -41
- package/dist/mcp-server/tools/gitClean/registration.js +19 -16
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
- package/dist/mcp-server/tools/gitClone/index.js +1 -1
- package/dist/mcp-server/tools/gitClone/logic.js +89 -30
- package/dist/mcp-server/tools/gitClone/registration.js +15 -12
- package/dist/mcp-server/tools/gitCommit/index.js +1 -1
- package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
- package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
- package/dist/mcp-server/tools/gitDiff/index.js +1 -1
- package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
- package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
- package/dist/mcp-server/tools/gitFetch/index.js +1 -1
- package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
- package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
- package/dist/mcp-server/tools/gitInit/index.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +88 -34
- package/dist/mcp-server/tools/gitInit/registration.js +32 -18
- package/dist/mcp-server/tools/gitLog/index.js +1 -1
- package/dist/mcp-server/tools/gitLog/logic.js +133 -47
- package/dist/mcp-server/tools/gitLog/registration.js +16 -14
- package/dist/mcp-server/tools/gitMerge/index.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
- package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
- package/dist/mcp-server/tools/gitPull/index.js +1 -1
- package/dist/mcp-server/tools/gitPull/logic.js +90 -69
- package/dist/mcp-server/tools/gitPull/registration.js +16 -14
- package/dist/mcp-server/tools/gitPush/index.js +1 -1
- package/dist/mcp-server/tools/gitPush/logic.js +116 -100
- package/dist/mcp-server/tools/gitPush/registration.js +16 -14
- package/dist/mcp-server/tools/gitRebase/index.js +1 -1
- package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
- package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
- package/dist/mcp-server/tools/gitRemote/index.js +1 -1
- package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
- package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
- package/dist/mcp-server/tools/gitReset/index.js +1 -1
- package/dist/mcp-server/tools/gitReset/logic.js +65 -37
- package/dist/mcp-server/tools/gitReset/registration.js +14 -12
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
- package/dist/mcp-server/tools/gitShow/index.js +1 -1
- package/dist/mcp-server/tools/gitShow/logic.js +78 -35
- package/dist/mcp-server/tools/gitShow/registration.js +17 -12
- package/dist/mcp-server/tools/gitStash/index.js +1 -1
- package/dist/mcp-server/tools/gitStash/logic.js +143 -58
- package/dist/mcp-server/tools/gitStash/registration.js +19 -12
- package/dist/mcp-server/tools/gitStatus/index.js +1 -1
- package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
- package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
- package/dist/mcp-server/tools/gitTag/index.js +1 -1
- package/dist/mcp-server/tools/gitTag/logic.js +124 -51
- package/dist/mcp-server/tools/gitTag/registration.js +14 -11
- package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
- package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
- package/dist/mcp-server/transports/httpTransport.js +187 -79
- package/dist/mcp-server/transports/stdioTransport.js +14 -8
- package/dist/types-global/errors.js +9 -4
- package/dist/utils/index.js +4 -4
- package/dist/utils/internal/errorHandler.js +62 -40
- package/dist/utils/internal/index.js +3 -3
- package/dist/utils/internal/logger.js +97 -54
- package/dist/utils/internal/requestContext.js +7 -5
- package/dist/utils/metrics/index.js +1 -1
- package/dist/utils/metrics/tokenCounter.js +18 -14
- package/dist/utils/parsing/dateParser.js +5 -5
- package/dist/utils/parsing/index.js +2 -2
- package/dist/utils/parsing/jsonParser.js +20 -11
- package/dist/utils/security/idGenerator.js +8 -10
- package/dist/utils/security/index.js +3 -3
- package/dist/utils/security/rateLimiter.js +16 -14
- package/dist/utils/security/sanitization.js +139 -82
- package/package.json +45 -23
|
@@ -1,26 +1,49 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { BaseErrorCode, McpError } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
5
5
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
6
|
-
import { logger } from
|
|
6
|
+
import { logger } from "../../../utils/index.js";
|
|
7
7
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
8
|
-
import { sanitization } from
|
|
8
|
+
import { sanitization } from "../../../utils/index.js";
|
|
9
9
|
// Import config to check signing flag
|
|
10
|
-
import { config } from
|
|
10
|
+
import { config } from "../../../config/index.js";
|
|
11
11
|
const execAsync = promisify(exec);
|
|
12
12
|
// Define the input schema for the git_commit tool using Zod
|
|
13
13
|
export const GitCommitInputSchema = z.object({
|
|
14
|
-
path: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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."),
|
|
24
47
|
});
|
|
25
48
|
/**
|
|
26
49
|
* Executes the 'git commit' command and returns structured JSON output.
|
|
@@ -30,16 +53,18 @@ export const GitCommitInputSchema = z.object({
|
|
|
30
53
|
* @returns {Promise<GitCommitResult>} A promise that resolves with the structured commit result.
|
|
31
54
|
* @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails unexpectedly.
|
|
32
55
|
*/
|
|
33
|
-
export async function commitGitChanges(input, context
|
|
34
|
-
|
|
35
|
-
const operation = 'commitGitChanges';
|
|
56
|
+
export async function commitGitChanges(input, context) {
|
|
57
|
+
const operation = "commitGitChanges";
|
|
36
58
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
37
59
|
let targetPath;
|
|
38
60
|
try {
|
|
39
61
|
// Resolve the target path
|
|
40
|
-
if (input.path && input.path !==
|
|
62
|
+
if (input.path && input.path !== ".") {
|
|
41
63
|
targetPath = input.path;
|
|
42
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
64
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
65
|
+
...context,
|
|
66
|
+
operation,
|
|
67
|
+
});
|
|
43
68
|
}
|
|
44
69
|
else {
|
|
45
70
|
const workingDir = context.getWorkingDirectory();
|
|
@@ -47,15 +72,29 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
47
72
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
48
73
|
}
|
|
49
74
|
targetPath = workingDir;
|
|
50
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
75
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
76
|
+
...context,
|
|
77
|
+
operation,
|
|
78
|
+
sessionId: context.sessionId,
|
|
79
|
+
});
|
|
51
80
|
}
|
|
52
81
|
// Sanitize the resolved path
|
|
53
|
-
const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
|
|
54
|
-
|
|
82
|
+
const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
|
|
83
|
+
allowAbsolute: true,
|
|
84
|
+
});
|
|
85
|
+
logger.debug("Sanitized path", {
|
|
86
|
+
...context,
|
|
87
|
+
operation,
|
|
88
|
+
sanitizedPathInfo,
|
|
89
|
+
});
|
|
55
90
|
targetPath = sanitizedPathInfo.sanitizedPath; // Use the sanitized path going forward
|
|
56
91
|
}
|
|
57
92
|
catch (error) {
|
|
58
|
-
logger.error(
|
|
93
|
+
logger.error("Path resolution or sanitization failed", {
|
|
94
|
+
...context,
|
|
95
|
+
operation,
|
|
96
|
+
error,
|
|
97
|
+
});
|
|
59
98
|
if (error instanceof McpError) {
|
|
60
99
|
throw error;
|
|
61
100
|
}
|
|
@@ -64,18 +103,30 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
64
103
|
try {
|
|
65
104
|
// --- Stage specific files if requested ---
|
|
66
105
|
if (input.filesToStage && input.filesToStage.length > 0) {
|
|
67
|
-
logger.debug(`Attempting to stage specific files: ${input.filesToStage.join(
|
|
106
|
+
logger.debug(`Attempting to stage specific files: ${input.filesToStage.join(", ")}`, { ...context, operation });
|
|
68
107
|
try {
|
|
69
108
|
// Correctly pass targetPath as rootDir in options object
|
|
70
|
-
const sanitizedFiles = input.filesToStage.map(file => sanitization.sanitizePath(file, { rootDir: targetPath })
|
|
71
|
-
|
|
109
|
+
const sanitizedFiles = input.filesToStage.map((file) => sanitization.sanitizePath(file, { rootDir: targetPath })
|
|
110
|
+
.sanitizedPath); // Sanitize relative to repo root
|
|
111
|
+
const filesToAddString = sanitizedFiles
|
|
112
|
+
.map((file) => `"${file}"`)
|
|
113
|
+
.join(" "); // Quote paths for safety
|
|
72
114
|
const addCommand = `git -C "${targetPath}" add -- ${filesToAddString}`;
|
|
73
|
-
logger.debug(`Executing git add command: ${addCommand}`, {
|
|
115
|
+
logger.debug(`Executing git add command: ${addCommand}`, {
|
|
116
|
+
...context,
|
|
117
|
+
operation,
|
|
118
|
+
});
|
|
74
119
|
await execAsync(addCommand);
|
|
75
|
-
logger.info(`Successfully staged specified files: ${sanitizedFiles.join(
|
|
120
|
+
logger.info(`Successfully staged specified files: ${sanitizedFiles.join(", ")}`, { ...context, operation });
|
|
76
121
|
}
|
|
77
122
|
catch (addError) {
|
|
78
|
-
logger.error(
|
|
123
|
+
logger.error("Failed to stage specified files", {
|
|
124
|
+
...context,
|
|
125
|
+
operation,
|
|
126
|
+
files: input.filesToStage,
|
|
127
|
+
error: addError.message,
|
|
128
|
+
stderr: addError.stderr,
|
|
129
|
+
});
|
|
79
130
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to stage files before commit: ${addError.stderr || addError.message}`, { context, operation, originalError: addError });
|
|
80
131
|
}
|
|
81
132
|
}
|
|
@@ -83,16 +134,20 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
83
134
|
// Escape message for shell safety
|
|
84
135
|
const escapeShellArg = (arg) => {
|
|
85
136
|
// Escape backslashes first, then other special chars
|
|
86
|
-
return arg
|
|
137
|
+
return arg
|
|
138
|
+
.replace(/\\/g, "\\\\")
|
|
139
|
+
.replace(/"/g, '\\"')
|
|
140
|
+
.replace(/`/g, "\\`")
|
|
141
|
+
.replace(/\$/g, "\\$");
|
|
87
142
|
};
|
|
88
143
|
const escapedMessage = escapeShellArg(input.message);
|
|
89
144
|
// Construct the git commit command using the resolved targetPath
|
|
90
145
|
let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
|
|
91
146
|
if (input.allowEmpty) {
|
|
92
|
-
command +=
|
|
147
|
+
command += " --allow-empty";
|
|
93
148
|
}
|
|
94
149
|
if (input.amend) {
|
|
95
|
-
command +=
|
|
150
|
+
command += " --amend --no-edit";
|
|
96
151
|
}
|
|
97
152
|
if (input.author) {
|
|
98
153
|
// Escape author details as well
|
|
@@ -102,16 +157,19 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
102
157
|
command = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
|
|
103
158
|
}
|
|
104
159
|
// Append common flags (ensure they are appended to the potentially modified command from author block)
|
|
105
|
-
if (input.allowEmpty && !command.includes(
|
|
106
|
-
command +=
|
|
107
|
-
if (input.amend && !command.includes(
|
|
108
|
-
command +=
|
|
160
|
+
if (input.allowEmpty && !command.includes(" --allow-empty"))
|
|
161
|
+
command += " --allow-empty";
|
|
162
|
+
if (input.amend && !command.includes(" --amend"))
|
|
163
|
+
command += " --amend --no-edit"; // Avoid double adding if author block modified command
|
|
109
164
|
// Append signing flag if configured via GIT_SIGN_COMMITS env var
|
|
110
165
|
if (config.gitSignCommits) {
|
|
111
|
-
command +=
|
|
112
|
-
logger.info(
|
|
166
|
+
command += " -S"; // Add signing flag (-S)
|
|
167
|
+
logger.info("Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.", { ...context, operation });
|
|
113
168
|
}
|
|
114
|
-
logger.debug(`Executing initial command attempt: ${command}`, {
|
|
169
|
+
logger.debug(`Executing initial command attempt: ${command}`, {
|
|
170
|
+
...context,
|
|
171
|
+
operation,
|
|
172
|
+
});
|
|
115
173
|
let stdout;
|
|
116
174
|
let stderr;
|
|
117
175
|
let commitResult;
|
|
@@ -122,29 +180,34 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
122
180
|
stderr = execResult.stderr;
|
|
123
181
|
}
|
|
124
182
|
catch (error) {
|
|
125
|
-
const initialErrorMessage = error.stderr || error.message ||
|
|
126
|
-
const isSigningError = initialErrorMessage.includes(
|
|
183
|
+
const initialErrorMessage = error.stderr || error.message || "";
|
|
184
|
+
const isSigningError = initialErrorMessage.includes("gpg failed to sign") ||
|
|
185
|
+
initialErrorMessage.includes("signing failed");
|
|
127
186
|
if (isSigningError && input.forceUnsignedOnFailure) {
|
|
128
|
-
logger.warning(
|
|
187
|
+
logger.warning("Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.", { ...context, operation, initialError: initialErrorMessage });
|
|
129
188
|
// Construct command *without* -S flag, using escaped message/author
|
|
130
189
|
const escapeShellArg = (arg) => {
|
|
131
|
-
return arg
|
|
190
|
+
return arg
|
|
191
|
+
.replace(/\\/g, "\\\\")
|
|
192
|
+
.replace(/"/g, '\\"')
|
|
193
|
+
.replace(/`/g, "\\`")
|
|
194
|
+
.replace(/\$/g, "\\$");
|
|
132
195
|
};
|
|
133
196
|
const escapedMessage = escapeShellArg(input.message);
|
|
134
197
|
let unsignedCommand = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
|
|
135
198
|
if (input.allowEmpty)
|
|
136
|
-
unsignedCommand +=
|
|
199
|
+
unsignedCommand += " --allow-empty";
|
|
137
200
|
if (input.amend)
|
|
138
|
-
unsignedCommand +=
|
|
201
|
+
unsignedCommand += " --amend --no-edit";
|
|
139
202
|
if (input.author) {
|
|
140
203
|
const escapedAuthorName = escapeShellArg(input.author.name);
|
|
141
204
|
const escapedAuthorEmail = escapeShellArg(input.author.email);
|
|
142
205
|
unsignedCommand = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
|
|
143
206
|
// Re-append common flags if author block overwrote command
|
|
144
|
-
if (input.allowEmpty && !unsignedCommand.includes(
|
|
145
|
-
unsignedCommand +=
|
|
146
|
-
if (input.amend && !unsignedCommand.includes(
|
|
147
|
-
unsignedCommand +=
|
|
207
|
+
if (input.allowEmpty && !unsignedCommand.includes(" --allow-empty"))
|
|
208
|
+
unsignedCommand += " --allow-empty";
|
|
209
|
+
if (input.amend && !unsignedCommand.includes(" --amend"))
|
|
210
|
+
unsignedCommand += " --amend --no-edit";
|
|
148
211
|
}
|
|
149
212
|
logger.debug(`Executing unsigned fallback command: ${unsignedCommand}`, { ...context, operation });
|
|
150
213
|
try {
|
|
@@ -156,12 +219,17 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
156
219
|
commitResult = {
|
|
157
220
|
success: true,
|
|
158
221
|
statusMessage: `Commit successful (unsigned, signing failed): ${stdout.trim()}`, // Default message, hash parsed below
|
|
159
|
-
commitHash: undefined // Will be parsed below
|
|
222
|
+
commitHash: undefined, // Will be parsed below
|
|
160
223
|
};
|
|
161
224
|
}
|
|
162
225
|
catch (fallbackError) {
|
|
163
226
|
// If the unsigned commit *also* fails, re-throw that error
|
|
164
|
-
logger.error(
|
|
227
|
+
logger.error("Unsigned fallback commit attempt also failed.", {
|
|
228
|
+
...context,
|
|
229
|
+
operation,
|
|
230
|
+
fallbackError: fallbackError.message,
|
|
231
|
+
stderr: fallbackError.stderr,
|
|
232
|
+
});
|
|
165
233
|
throw fallbackError; // Re-throw the error from the unsigned attempt
|
|
166
234
|
}
|
|
167
235
|
}
|
|
@@ -172,15 +240,23 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
172
240
|
}
|
|
173
241
|
// Process result (either from initial attempt or fallback)
|
|
174
242
|
// Check stderr first for common non-error messages
|
|
175
|
-
if (stderr && !commitResult) {
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
if (stderr && !commitResult) {
|
|
244
|
+
// Don't overwrite fallback message if stderr also exists
|
|
245
|
+
if (stderr.includes("nothing to commit, working tree clean") ||
|
|
246
|
+
stderr.includes("no changes added to commit")) {
|
|
247
|
+
const msg = stderr.includes("nothing to commit")
|
|
248
|
+
? "Nothing to commit, working tree clean."
|
|
249
|
+
: "No changes added to commit.";
|
|
178
250
|
logger.info(msg, { ...context, operation, path: targetPath });
|
|
179
251
|
// Use statusMessage
|
|
180
252
|
return { success: true, statusMessage: msg, nothingToCommit: true };
|
|
181
253
|
}
|
|
182
254
|
// Log other stderr as warning but continue, as commit might still succeed
|
|
183
|
-
logger.warning(`Git commit command produced stderr`, {
|
|
255
|
+
logger.warning(`Git commit command produced stderr`, {
|
|
256
|
+
...context,
|
|
257
|
+
operation,
|
|
258
|
+
stderr,
|
|
259
|
+
});
|
|
184
260
|
}
|
|
185
261
|
// Extract commit hash (more robustly)
|
|
186
262
|
let commitHash = undefined;
|
|
@@ -190,51 +266,97 @@ export async function commitGitChanges(input, context // Add getter to context
|
|
|
190
266
|
}
|
|
191
267
|
else {
|
|
192
268
|
// Fallback parsing if needed, or rely on success message
|
|
193
|
-
logger.warning(
|
|
269
|
+
logger.warning("Could not parse commit hash from stdout", {
|
|
270
|
+
...context,
|
|
271
|
+
operation,
|
|
272
|
+
stdout,
|
|
273
|
+
});
|
|
194
274
|
}
|
|
195
275
|
// Use statusMessage, potentially using the one set during fallback
|
|
196
|
-
const finalStatusMsg = commitResult?.statusMessage ||
|
|
197
|
-
|
|
198
|
-
|
|
276
|
+
const finalStatusMsg = commitResult?.statusMessage ||
|
|
277
|
+
(commitHash
|
|
278
|
+
? `Commit successful: ${commitHash}`
|
|
279
|
+
: `Commit successful (stdout: ${stdout.trim()})`);
|
|
199
280
|
let committedFiles = [];
|
|
200
281
|
if (commitHash) {
|
|
201
282
|
try {
|
|
202
283
|
// Get the list of files included in this specific commit
|
|
203
284
|
const showCommand = `git -C "${targetPath}" show --pretty="" --name-only ${commitHash}`;
|
|
204
|
-
logger.debug(`Executing git show command: ${showCommand}`, {
|
|
285
|
+
logger.debug(`Executing git show command: ${showCommand}`, {
|
|
286
|
+
...context,
|
|
287
|
+
operation,
|
|
288
|
+
});
|
|
205
289
|
const { stdout: showStdout } = await execAsync(showCommand);
|
|
206
|
-
committedFiles = showStdout.trim().split(
|
|
207
|
-
logger.debug(`Retrieved committed files list for ${commitHash}`, {
|
|
290
|
+
committedFiles = showStdout.trim().split("\n").filter(Boolean); // Split by newline, remove empty lines
|
|
291
|
+
logger.debug(`Retrieved committed files list for ${commitHash}`, {
|
|
292
|
+
...context,
|
|
293
|
+
operation,
|
|
294
|
+
count: committedFiles.length,
|
|
295
|
+
});
|
|
208
296
|
}
|
|
209
297
|
catch (showError) {
|
|
210
298
|
// Log a warning but don't fail the overall operation if we can't get the file list
|
|
211
|
-
logger.warning(
|
|
299
|
+
logger.warning("Failed to retrieve committed files list", {
|
|
300
|
+
...context,
|
|
301
|
+
operation,
|
|
302
|
+
commitHash,
|
|
303
|
+
error: showError.message,
|
|
304
|
+
stderr: showError.stderr,
|
|
305
|
+
});
|
|
212
306
|
}
|
|
213
307
|
}
|
|
214
|
-
|
|
308
|
+
const successMessage = `Commit successful: ${commitHash}`;
|
|
309
|
+
logger.info(successMessage, {
|
|
310
|
+
...context,
|
|
311
|
+
operation,
|
|
312
|
+
path: targetPath,
|
|
313
|
+
commitHash,
|
|
314
|
+
signed: !commitResult, // Log if it was signed (not fallback)
|
|
315
|
+
committedFilesCount: committedFiles.length,
|
|
316
|
+
});
|
|
215
317
|
return {
|
|
216
318
|
success: true,
|
|
217
319
|
statusMessage: finalStatusMsg, // Use potentially modified message
|
|
218
320
|
commitHash: commitHash,
|
|
219
321
|
commitMessage: input.message, // Include the original commit message
|
|
220
|
-
committedFiles: committedFiles // Include the list of files
|
|
322
|
+
committedFiles: committedFiles, // Include the list of files
|
|
221
323
|
};
|
|
222
324
|
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
|
|
225
|
-
|
|
325
|
+
catch (error) {
|
|
326
|
+
// This catch block now primarily handles non-signing errors or errors from the fallback attempt
|
|
327
|
+
logger.error(`Failed to execute git commit command`, {
|
|
328
|
+
...context,
|
|
329
|
+
operation,
|
|
330
|
+
path: targetPath,
|
|
331
|
+
error: error.message,
|
|
332
|
+
stderr: error.stderr,
|
|
333
|
+
});
|
|
334
|
+
const errorMessage = error.stderr || error.message || "";
|
|
226
335
|
// Handle specific error cases first
|
|
227
|
-
if (errorMessage.toLowerCase().includes(
|
|
336
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
228
337
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
229
338
|
}
|
|
230
|
-
|
|
339
|
+
// Check for pre-commit hook failures before checking for generic conflicts
|
|
340
|
+
if (errorMessage.toLowerCase().includes("pre-commit hook") ||
|
|
341
|
+
errorMessage.toLowerCase().includes("hook failed")) {
|
|
342
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Commit failed due to pre-commit hook failure. Details: ${errorMessage}`, { context, operation, originalError: error });
|
|
343
|
+
}
|
|
344
|
+
if (errorMessage.includes("nothing to commit") ||
|
|
345
|
+
errorMessage.includes("no changes added to commit")) {
|
|
231
346
|
// This might happen if git exits with error despite these messages
|
|
232
|
-
const msg = errorMessage.includes(
|
|
233
|
-
|
|
347
|
+
const msg = errorMessage.includes("nothing to commit")
|
|
348
|
+
? "Nothing to commit, working tree clean."
|
|
349
|
+
: "No changes added to commit.";
|
|
350
|
+
logger.info(msg + " (caught as error)", {
|
|
351
|
+
...context,
|
|
352
|
+
operation,
|
|
353
|
+
path: targetPath,
|
|
354
|
+
errorMessage,
|
|
355
|
+
});
|
|
234
356
|
// Return success=false but indicate the reason using statusMessage
|
|
235
357
|
return { success: false, statusMessage: msg, nothingToCommit: true };
|
|
236
358
|
}
|
|
237
|
-
if (errorMessage.includes(
|
|
359
|
+
if (errorMessage.includes("conflicts")) {
|
|
238
360
|
throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
|
|
239
361
|
}
|
|
240
362
|
// Generic internal error for other failures
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
-
import { ErrorHandler } from
|
|
2
|
+
import { ErrorHandler } from "../../../utils/index.js";
|
|
3
3
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
-
import { logger } from
|
|
4
|
+
import { logger } from "../../../utils/index.js";
|
|
5
5
|
// Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
|
|
6
|
-
import { requestContextService } from
|
|
6
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
7
7
|
// Import the result type along with the function and input schema
|
|
8
|
-
import { BaseErrorCode } from
|
|
9
|
-
import { commitGitChanges, GitCommitInputSchema } from
|
|
8
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
9
|
+
import { commitGitChanges, GitCommitInputSchema, } from "./logic.js";
|
|
10
10
|
let _getWorkingDirectory;
|
|
11
11
|
let _getSessionId;
|
|
12
12
|
/**
|
|
@@ -18,9 +18,9 @@ let _getSessionId;
|
|
|
18
18
|
export function initializeGitCommitStateAccessors(getWdFn, getSidFn) {
|
|
19
19
|
_getWorkingDirectory = getWdFn;
|
|
20
20
|
_getSessionId = getSidFn;
|
|
21
|
-
logger.info(
|
|
21
|
+
logger.info("State accessors initialized for git_commit tool registration.");
|
|
22
22
|
}
|
|
23
|
-
const TOOL_NAME =
|
|
23
|
+
const TOOL_NAME = "git_commit";
|
|
24
24
|
const TOOL_DESCRIPTION = `Commits staged changes to the Git repository index with a descriptive message. Supports author override, amending, and empty commits. Returns a JSON result.
|
|
25
25
|
|
|
26
26
|
**Commit Message Guidance:**
|
|
@@ -45,24 +45,27 @@ Closes #123 (if applicable).
|
|
|
45
45
|
- Commit related changes logically. Use the optional \`filesToStage\` parameter to auto-stage specific files before committing.
|
|
46
46
|
- The \`path\` defaults to the session's working directory unless overridden. If \`GIT_SIGN_COMMITS=true\` is set, commits are signed (\`-S\`), with an optional \`forceUnsignedOnFailure\` fallback.`;
|
|
47
47
|
/**
|
|
48
|
-
* Registers the git_commit tool with the MCP server.
|
|
49
|
-
* Uses the high-level server.tool() method for registration, schema validation, and routing.
|
|
50
|
-
*
|
|
51
|
-
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
52
|
-
* @returns {Promise<void>}
|
|
53
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
54
|
-
*/
|
|
48
|
+
* Registers the git_commit tool with the MCP server.
|
|
49
|
+
* Uses the high-level server.tool() method for registration, schema validation, and routing.
|
|
50
|
+
*
|
|
51
|
+
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
54
|
+
*/
|
|
55
55
|
export const registerGitCommitTool = async (server) => {
|
|
56
56
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
57
|
-
throw new Error(
|
|
57
|
+
throw new Error("State accessors for git_commit must be initialized before registration.");
|
|
58
58
|
}
|
|
59
|
-
const operation =
|
|
59
|
+
const operation = "registerGitCommitTool";
|
|
60
60
|
const context = requestContextService.createRequestContext({ operation });
|
|
61
61
|
await ErrorHandler.tryCatch(async () => {
|
|
62
62
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCommitInputSchema.shape, // Provide the Zod schema shape
|
|
63
63
|
async (validatedArgs, callContext) => {
|
|
64
|
-
const toolOperation =
|
|
65
|
-
const requestContext = requestContextService.createRequestContext({
|
|
64
|
+
const toolOperation = "tool:git_commit";
|
|
65
|
+
const requestContext = requestContextService.createRequestContext({
|
|
66
|
+
operation: toolOperation,
|
|
67
|
+
parentContext: callContext,
|
|
68
|
+
});
|
|
66
69
|
const sessionId = _getSessionId(requestContext);
|
|
67
70
|
const getWorkingDirectoryForSession = () => {
|
|
68
71
|
return _getWorkingDirectory(sessionId);
|
|
@@ -78,10 +81,10 @@ export const registerGitCommitTool = async (server) => {
|
|
|
78
81
|
const commitResult = await commitGitChanges(validatedArgs, logicContext);
|
|
79
82
|
// Format the result as a JSON string within TextContent
|
|
80
83
|
const resultContent = {
|
|
81
|
-
type:
|
|
84
|
+
type: "text",
|
|
82
85
|
// Stringify the JSON object for the response content
|
|
83
86
|
text: JSON.stringify(commitResult, null, 2), // Pretty-print JSON
|
|
84
|
-
contentType:
|
|
87
|
+
contentType: "application/json",
|
|
85
88
|
};
|
|
86
89
|
// Log based on the success flag in the result
|
|
87
90
|
if (commitResult.success) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitDiff tool.
|
|
3
3
|
*/
|
|
4
|
-
export { registerGitDiffTool, initializeGitDiffStateAccessors } from
|
|
4
|
+
export { registerGitDiffTool, initializeGitDiffStateAccessors, } from "./registration.js";
|
|
5
5
|
// Export types if needed elsewhere, e.g.:
|
|
6
6
|
// export type { GitDiffInput, GitDiffResult } from './logic.js';
|