@cyanheads/git-mcp-server 2.1.4 → 2.1.5
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/dist/mcp-server/tools/gitAdd/logic.js +18 -45
- package/dist/mcp-server/tools/gitBranch/logic.js +39 -34
- package/dist/mcp-server/tools/gitCheckout/logic.js +17 -12
- package/dist/mcp-server/tools/gitCherryPick/logic.js +22 -15
- package/dist/mcp-server/tools/gitClean/logic.js +11 -8
- package/dist/mcp-server/tools/gitClone/logic.js +15 -11
- package/dist/mcp-server/tools/gitCommit/logic.js +29 -65
- package/dist/mcp-server/tools/gitDiff/logic.js +29 -14
- package/dist/mcp-server/tools/gitFetch/logic.js +13 -12
- package/dist/mcp-server/tools/gitInit/logic.js +12 -9
- package/dist/mcp-server/tools/gitLog/logic.js +17 -30
- package/dist/mcp-server/tools/gitMerge/logic.js +17 -12
- package/dist/mcp-server/tools/gitPull/logic.js +13 -14
- package/dist/mcp-server/tools/gitPush/logic.js +19 -21
- package/dist/mcp-server/tools/gitRebase/logic.js +29 -20
- package/dist/mcp-server/tools/gitRemote/logic.js +15 -15
- package/dist/mcp-server/tools/gitReset/logic.js +11 -10
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +6 -4
- package/dist/mcp-server/tools/gitShow/logic.js +9 -8
- package/dist/mcp-server/tools/gitStash/logic.js +16 -17
- package/dist/mcp-server/tools/gitStatus/logic.js +10 -8
- package/dist/mcp-server/tools/gitTag/logic.js +15 -15
- package/dist/mcp-server/tools/gitWorktree/logic.js +54 -38
- package/package.json +2 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
7
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
9
|
import { sanitization } from "../../../utils/index.js";
|
|
10
|
-
const
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
11
|
// Define the input schema for the git_add tool using Zod
|
|
12
12
|
export const GitAddInputSchema = z.object({
|
|
13
13
|
path: z
|
|
@@ -76,52 +76,25 @@ export async function addGitFiles(input, context) {
|
|
|
76
76
|
}
|
|
77
77
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (filesToStage.length === 0) {
|
|
85
|
-
logger.warning("Empty array provided for files, defaulting to staging all changes.", { ...context, operation });
|
|
86
|
-
filesArg = "."; // Default to staging all if array is empty
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
// Quote each file path individually
|
|
90
|
-
filesArg = filesToStage
|
|
91
|
-
.map((file) => {
|
|
92
|
-
const sanitizedFile = file.startsWith("-") ? `./${file}` : file; // Prefix with './' if it starts with a dash
|
|
93
|
-
return `"${sanitizedFile.replace(/"/g, '\\"')}"`; // Escape quotes within path
|
|
94
|
-
})
|
|
95
|
-
.join(" ");
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
// Single string case
|
|
100
|
-
const sanitizedFile = filesToStage.startsWith("-")
|
|
101
|
-
? `./${filesToStage}`
|
|
102
|
-
: filesToStage; // Prefix with './' if it starts with a dash
|
|
103
|
-
filesArg = `"${sanitizedFile.replace(/"/g, '\\"')}"`;
|
|
104
|
-
}
|
|
79
|
+
const filesToStage = Array.isArray(input.files)
|
|
80
|
+
? input.files
|
|
81
|
+
: [input.files];
|
|
82
|
+
if (filesToStage.length === 0) {
|
|
83
|
+
filesToStage.push("."); // Default to staging all if array is empty
|
|
105
84
|
}
|
|
106
|
-
|
|
107
|
-
|
|
85
|
+
try {
|
|
86
|
+
const args = ["-C", targetPath, "add", "--"];
|
|
87
|
+
filesToStage.forEach((file) => {
|
|
88
|
+
// Sanitize each file path. Although execFile is safer,
|
|
89
|
+
// this prevents arguments like "-v" from being treated as flags by git.
|
|
90
|
+
const sanitizedFile = file.startsWith("-") ? `./${file}` : file;
|
|
91
|
+
args.push(sanitizedFile);
|
|
92
|
+
});
|
|
93
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
108
94
|
...context,
|
|
109
95
|
operation,
|
|
110
|
-
files: filesToStage,
|
|
111
|
-
error: err,
|
|
112
96
|
});
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
// This check should ideally not be needed now due to the logic above
|
|
116
|
-
if (!filesArg) {
|
|
117
|
-
logger.error("Internal error: filesArg is unexpectedly empty after processing.", { ...context, operation });
|
|
118
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Internal error preparing git add command.", { context, operation });
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
// Use the resolved targetPath
|
|
122
|
-
const command = `git -C "${targetPath}" add -- ${filesArg}`;
|
|
123
|
-
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
124
|
-
const { stdout, stderr } = await execAsync(command);
|
|
97
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
125
98
|
if (stderr) {
|
|
126
99
|
// Log stderr as warning, as 'git add' can produce warnings but still succeed.
|
|
127
100
|
logger.warning(`Git add command produced stderr`, {
|
|
@@ -162,7 +135,7 @@ export async function addGitFiles(input, context) {
|
|
|
162
135
|
}
|
|
163
136
|
if (errorMessage.toLowerCase().includes("did not match any files")) {
|
|
164
137
|
// Still throw an error, but return structured info in the catch block of the registration
|
|
165
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Specified files/patterns did not match any files in ${targetPath}: ${
|
|
138
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Specified files/patterns did not match any files in ${targetPath}: ${filesToStage.join(", ")}`, { context, operation, originalError: error, filesStaged: filesToStage });
|
|
166
139
|
}
|
|
167
140
|
// Throw generic error for other cases
|
|
168
141
|
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to stage files for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error, filesStaged: filesToStage });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
7
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
9
|
import { sanitization } from "../../../utils/index.js";
|
|
10
|
-
const
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
11
|
// Define the BASE input schema for the git_branch tool using Zod
|
|
12
12
|
export const GitBranchBaseSchema = z.object({
|
|
13
13
|
path: z
|
|
@@ -115,21 +115,23 @@ export async function gitBranchLogic(input, context) {
|
|
|
115
115
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
116
116
|
}
|
|
117
117
|
try {
|
|
118
|
-
let
|
|
118
|
+
let args;
|
|
119
119
|
let result;
|
|
120
120
|
switch (input.mode) {
|
|
121
121
|
case "list":
|
|
122
|
-
|
|
123
|
-
if (input.all)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
args = ["-C", targetPath, "branch", "--list", "--no-color"]; // Start with basic list
|
|
123
|
+
if (input.all) {
|
|
124
|
+
args.push("-a"); // Add -a if requested
|
|
125
|
+
}
|
|
126
|
+
else if (input.remote) {
|
|
127
|
+
args.push("-r"); // Add -r if requested (exclusive with -a)
|
|
128
|
+
}
|
|
129
|
+
args.push("--verbose"); // Add verbose for commit info
|
|
130
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
129
131
|
...context,
|
|
130
132
|
operation,
|
|
131
133
|
});
|
|
132
|
-
const { stdout: listStdout } = await
|
|
134
|
+
const { stdout: listStdout } = await execFileAsync("git", args);
|
|
133
135
|
const branches = listStdout
|
|
134
136
|
.trim()
|
|
135
137
|
.split("\n")
|
|
@@ -157,17 +159,19 @@ export async function gitBranchLogic(input, context) {
|
|
|
157
159
|
break;
|
|
158
160
|
case "create":
|
|
159
161
|
// branchName is validated by Zod refine
|
|
160
|
-
|
|
161
|
-
if (input.force)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
args = ["-C", targetPath, "branch"];
|
|
163
|
+
if (input.force) {
|
|
164
|
+
args.push("-f");
|
|
165
|
+
}
|
|
166
|
+
args.push(input.branchName); // branchName is guaranteed by refine
|
|
167
|
+
if (input.startPoint) {
|
|
168
|
+
args.push(input.startPoint);
|
|
169
|
+
}
|
|
170
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
167
171
|
...context,
|
|
168
172
|
operation,
|
|
169
173
|
});
|
|
170
|
-
await
|
|
174
|
+
await execFileAsync("git", args);
|
|
171
175
|
result = {
|
|
172
176
|
success: true,
|
|
173
177
|
mode: "create",
|
|
@@ -177,16 +181,17 @@ export async function gitBranchLogic(input, context) {
|
|
|
177
181
|
break;
|
|
178
182
|
case "delete":
|
|
179
183
|
// branchName is validated by Zod refine
|
|
180
|
-
|
|
181
|
-
if (input.remote)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
args = ["-C", targetPath, "branch"];
|
|
185
|
+
if (input.remote) {
|
|
186
|
+
args.push("-r");
|
|
187
|
+
}
|
|
188
|
+
args.push(input.force ? "-D" : "-d");
|
|
189
|
+
args.push(input.branchName); // branchName is guaranteed by refine
|
|
190
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
186
191
|
...context,
|
|
187
192
|
operation,
|
|
188
193
|
});
|
|
189
|
-
const { stdout: deleteStdout } = await
|
|
194
|
+
const { stdout: deleteStdout } = await execFileAsync("git", args);
|
|
190
195
|
result = {
|
|
191
196
|
success: true,
|
|
192
197
|
mode: "delete",
|
|
@@ -198,14 +203,14 @@ export async function gitBranchLogic(input, context) {
|
|
|
198
203
|
break;
|
|
199
204
|
case "rename":
|
|
200
205
|
// branchName and newBranchName validated by Zod refine
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
logger.debug(`Executing command: ${
|
|
206
|
+
args = ["-C", targetPath, "branch"];
|
|
207
|
+
args.push(input.force ? "-M" : "-m");
|
|
208
|
+
args.push(input.branchName, input.newBranchName);
|
|
209
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
205
210
|
...context,
|
|
206
211
|
operation,
|
|
207
212
|
});
|
|
208
|
-
await
|
|
213
|
+
await execFileAsync("git", args);
|
|
209
214
|
result = {
|
|
210
215
|
success: true,
|
|
211
216
|
mode: "rename",
|
|
@@ -215,13 +220,13 @@ export async function gitBranchLogic(input, context) {
|
|
|
215
220
|
};
|
|
216
221
|
break;
|
|
217
222
|
case "show-current":
|
|
218
|
-
|
|
219
|
-
logger.debug(`Executing command: ${
|
|
223
|
+
args = ["-C", targetPath, "branch", "--show-current"];
|
|
224
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
220
225
|
...context,
|
|
221
226
|
operation,
|
|
222
227
|
});
|
|
223
228
|
try {
|
|
224
|
-
const { stdout: currentStdout } = await
|
|
229
|
+
const { stdout: currentStdout } = await execFileAsync("git", args);
|
|
225
230
|
const currentBranchName = currentStdout.trim();
|
|
226
231
|
result = {
|
|
227
232
|
success: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
7
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
9
|
import { sanitization } from "../../../utils/index.js";
|
|
10
|
-
const
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
11
|
// Define the input schema for the git_checkout tool using Zod
|
|
12
12
|
export const GitCheckoutInputSchema = z.object({
|
|
13
13
|
path: z
|
|
@@ -75,22 +75,22 @@ export async function checkoutGit(input, context) {
|
|
|
75
75
|
throw error;
|
|
76
76
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
77
77
|
}
|
|
78
|
-
// Basic sanitization for branch/path argument
|
|
79
|
-
const safeBranchOrPath = input.branchOrPath.replace(/[`$&;*()|<>]/g, ""); // Remove potentially dangerous characters
|
|
80
78
|
try {
|
|
81
79
|
// Construct the git checkout command
|
|
82
|
-
|
|
80
|
+
const args = ["-C", targetPath, "checkout"];
|
|
83
81
|
if (input.force) {
|
|
84
|
-
|
|
82
|
+
args.push("--force");
|
|
85
83
|
}
|
|
86
84
|
if (input.newBranch) {
|
|
87
|
-
|
|
88
|
-
command += ` -b ${safeNewBranch}`;
|
|
85
|
+
args.push("-b", input.newBranch);
|
|
89
86
|
}
|
|
90
|
-
|
|
91
|
-
logger.debug(`Executing command: ${
|
|
87
|
+
args.push(input.branchOrPath); // Add the target branch/path
|
|
88
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
89
|
+
...context,
|
|
90
|
+
operation,
|
|
91
|
+
});
|
|
92
92
|
// Execute command. Checkout often uses stderr for status messages.
|
|
93
|
-
const { stdout, stderr } = await
|
|
93
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
94
94
|
const message = stderr.trim() || stdout.trim();
|
|
95
95
|
logger.debug(`Git checkout stdout: ${stdout}`, { ...context, operation });
|
|
96
96
|
if (stderr) {
|
|
@@ -99,7 +99,12 @@ export async function checkoutGit(input, context) {
|
|
|
99
99
|
// Get the current branch name after the checkout operation
|
|
100
100
|
let currentBranch;
|
|
101
101
|
try {
|
|
102
|
-
const { stdout: branchStdout } = await
|
|
102
|
+
const { stdout: branchStdout } = await execFileAsync("git", [
|
|
103
|
+
"-C",
|
|
104
|
+
targetPath,
|
|
105
|
+
"branch",
|
|
106
|
+
"--show-current",
|
|
107
|
+
]);
|
|
103
108
|
currentBranch = branchStdout.trim();
|
|
104
109
|
}
|
|
105
110
|
catch (e) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
7
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
9
|
import { sanitization } from "../../../utils/index.js";
|
|
10
|
-
const
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
11
|
// Define the input schema for the git_cherry-pick tool using Zod
|
|
12
12
|
export const GitCherryPickInputSchema = z.object({
|
|
13
13
|
path: z
|
|
@@ -95,20 +95,27 @@ export async function gitCherryPickLogic(input, context) {
|
|
|
95
95
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
96
96
|
}
|
|
97
97
|
try {
|
|
98
|
-
|
|
99
|
-
if (input.mainline)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (input.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
const args = ["-C", targetPath, "cherry-pick"];
|
|
99
|
+
if (input.mainline) {
|
|
100
|
+
args.push("-m", String(input.mainline));
|
|
101
|
+
}
|
|
102
|
+
if (input.strategy) {
|
|
103
|
+
args.push(`-X${input.strategy}`);
|
|
104
|
+
} // Note: -X for strategy options
|
|
105
|
+
if (input.noCommit) {
|
|
106
|
+
args.push("--no-commit");
|
|
107
|
+
}
|
|
108
|
+
if (input.signoff) {
|
|
109
|
+
args.push("--signoff");
|
|
110
|
+
}
|
|
111
|
+
// Add the commit reference(s)
|
|
112
|
+
args.push(input.commitRef);
|
|
113
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
114
|
+
...context,
|
|
115
|
+
operation,
|
|
116
|
+
});
|
|
110
117
|
try {
|
|
111
|
-
const { stdout, stderr } = await
|
|
118
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
112
119
|
// Check stdout/stderr for conflict messages, although exit code 0 usually means success
|
|
113
120
|
const output = stdout + stderr;
|
|
114
121
|
const conflicts = /conflict/i.test(output);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
7
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
8
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
9
|
import { sanitization } from "../../../utils/index.js";
|
|
10
|
-
const
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
11
|
// Define the input schema for the git_clean tool using Zod
|
|
12
12
|
// No refinements needed here, but the 'force' check is critical in the logic
|
|
13
13
|
export const GitCleanInputSchema = z.object({
|
|
@@ -102,18 +102,21 @@ export async function gitCleanLogic(input, context) {
|
|
|
102
102
|
try {
|
|
103
103
|
// Construct the command
|
|
104
104
|
// Force (-f) is always added because the logic checks input.force
|
|
105
|
-
|
|
105
|
+
const args = ["-C", targetPath, "clean", "-f"];
|
|
106
106
|
if (input.dryRun) {
|
|
107
|
-
|
|
107
|
+
args.push("-n");
|
|
108
108
|
}
|
|
109
109
|
if (input.directories) {
|
|
110
|
-
|
|
110
|
+
args.push("-d");
|
|
111
111
|
}
|
|
112
112
|
if (input.ignored) {
|
|
113
|
-
|
|
113
|
+
args.push("-x");
|
|
114
114
|
}
|
|
115
|
-
logger.debug(`Executing command: ${
|
|
116
|
-
|
|
115
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
116
|
+
...context,
|
|
117
|
+
operation,
|
|
118
|
+
});
|
|
119
|
+
const { stdout, stderr } = await execFileAsync("git", args);
|
|
117
120
|
if (stderr) {
|
|
118
121
|
// Log stderr as warning, as git clean might report non-fatal issues here
|
|
119
122
|
logger.warning(`Git clean command produced stderr`, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import { promisify } from "util";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -8,7 +8,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
8
8
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
9
9
|
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
10
10
|
import { sanitization } from "../../../utils/index.js";
|
|
11
|
-
const
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
12
|
// Define the input schema for the git_clone tool using Zod
|
|
13
13
|
export const GitCloneInputSchema = z.object({
|
|
14
14
|
repositoryUrl: z
|
|
@@ -108,22 +108,26 @@ export async function gitCloneLogic(input, context) {
|
|
|
108
108
|
}
|
|
109
109
|
try {
|
|
110
110
|
// Construct the git clone command
|
|
111
|
-
|
|
112
|
-
let command = `git clone`;
|
|
111
|
+
const args = ["clone"];
|
|
113
112
|
if (input.quiet) {
|
|
114
|
-
|
|
113
|
+
args.push("--quiet");
|
|
115
114
|
}
|
|
116
115
|
if (input.branch) {
|
|
117
|
-
|
|
116
|
+
args.push("--branch", input.branch);
|
|
118
117
|
}
|
|
119
118
|
if (input.depth) {
|
|
120
|
-
|
|
119
|
+
args.push("--depth", String(input.depth));
|
|
121
120
|
}
|
|
122
|
-
// Add repo URL and target path
|
|
123
|
-
|
|
124
|
-
logger.debug(`Executing command: ${
|
|
121
|
+
// Add repo URL and target path
|
|
122
|
+
args.push(sanitizedRepoUrl, sanitizedTargetPath);
|
|
123
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
124
|
+
...context,
|
|
125
|
+
operation,
|
|
126
|
+
});
|
|
125
127
|
// Increase timeout for clone operations as they can take time
|
|
126
|
-
const { stdout, stderr } = await
|
|
128
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
129
|
+
timeout: 300000,
|
|
130
|
+
}); // 5 minutes timeout
|
|
127
131
|
if (stderr && !input.quiet) {
|
|
128
132
|
// Stderr often contains progress info, log as info if quiet is false
|
|
129
133
|
logger.info(`Git clone command produced stderr (progress/info)`, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
@@ -8,7 +8,7 @@ import { logger } from "../../../utils/index.js";
|
|
|
8
8
|
import { sanitization } from "../../../utils/index.js";
|
|
9
9
|
// Import config to check signing flag
|
|
10
10
|
import { config } from "../../../config/index.js";
|
|
11
|
-
const
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
12
|
// Define the input schema for the git_commit tool using Zod
|
|
13
13
|
export const GitCommitInputSchema = z.object({
|
|
14
14
|
path: z
|
|
@@ -108,15 +108,12 @@ export async function commitGitChanges(input, context) {
|
|
|
108
108
|
// Correctly pass targetPath as rootDir in options object
|
|
109
109
|
const sanitizedFiles = input.filesToStage.map((file) => sanitization.sanitizePath(file, { rootDir: targetPath })
|
|
110
110
|
.sanitizedPath); // Sanitize relative to repo root
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
.join(" "); // Quote paths for safety
|
|
114
|
-
const addCommand = `git -C "${targetPath}" add -- ${filesToAddString}`;
|
|
115
|
-
logger.debug(`Executing git add command: ${addCommand}`, {
|
|
111
|
+
const addArgs = ["-C", targetPath, "add", "--", ...sanitizedFiles];
|
|
112
|
+
logger.debug(`Executing git add command: git ${addArgs.join(" ")}`, {
|
|
116
113
|
...context,
|
|
117
114
|
operation,
|
|
118
115
|
});
|
|
119
|
-
await
|
|
116
|
+
await execFileAsync("git", addArgs);
|
|
120
117
|
logger.info(`Successfully staged specified files: ${sanitizedFiles.join(", ")}`, { ...context, operation });
|
|
121
118
|
}
|
|
122
119
|
catch (addError) {
|
|
@@ -131,42 +128,24 @@ export async function commitGitChanges(input, context) {
|
|
|
131
128
|
}
|
|
132
129
|
}
|
|
133
130
|
// --- End staging files ---
|
|
134
|
-
// Escape message for shell safety
|
|
135
|
-
const escapeShellArg = (arg) => {
|
|
136
|
-
// Escape backslashes first, then other special chars
|
|
137
|
-
return arg
|
|
138
|
-
.replace(/\\/g, "\\\\")
|
|
139
|
-
.replace(/"/g, '\\"')
|
|
140
|
-
.replace(/`/g, "\\`")
|
|
141
|
-
.replace(/\$/g, "\\$");
|
|
142
|
-
};
|
|
143
|
-
const escapedMessage = escapeShellArg(input.message);
|
|
144
131
|
// Construct the git commit command using the resolved targetPath
|
|
145
|
-
|
|
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);
|
|
146
137
|
if (input.allowEmpty) {
|
|
147
|
-
|
|
138
|
+
args.push("--allow-empty");
|
|
148
139
|
}
|
|
149
140
|
if (input.amend) {
|
|
150
|
-
|
|
141
|
+
args.push("--amend", "--no-edit");
|
|
151
142
|
}
|
|
152
|
-
if (input.author) {
|
|
153
|
-
// Escape author details as well
|
|
154
|
-
const escapedAuthorName = escapeShellArg(input.author.name);
|
|
155
|
-
const escapedAuthorEmail = escapeShellArg(input.author.email); // Email typically safe, but escape anyway
|
|
156
|
-
// Use -c flags to override author for this commit, using the already escaped message
|
|
157
|
-
command = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
|
|
158
|
-
}
|
|
159
|
-
// Append common flags (ensure they are appended to the potentially modified command from author block)
|
|
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
|
|
164
143
|
// Append signing flag if configured via GIT_SIGN_COMMITS env var
|
|
165
144
|
if (config.gitSignCommits) {
|
|
166
|
-
|
|
145
|
+
args.push("-S"); // Add signing flag (-S)
|
|
167
146
|
logger.info("Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.", { ...context, operation });
|
|
168
147
|
}
|
|
169
|
-
logger.debug(`Executing initial command attempt: ${
|
|
148
|
+
logger.debug(`Executing initial command attempt: git ${args.join(" ")}`, {
|
|
170
149
|
...context,
|
|
171
150
|
operation,
|
|
172
151
|
});
|
|
@@ -175,7 +154,7 @@ export async function commitGitChanges(input, context) {
|
|
|
175
154
|
let commitResult;
|
|
176
155
|
try {
|
|
177
156
|
// Initial attempt (potentially with -S flag)
|
|
178
|
-
const execResult = await
|
|
157
|
+
const execResult = await execFileAsync("git", args);
|
|
179
158
|
stdout = execResult.stdout;
|
|
180
159
|
stderr = execResult.stderr;
|
|
181
160
|
}
|
|
@@ -185,34 +164,12 @@ export async function commitGitChanges(input, context) {
|
|
|
185
164
|
initialErrorMessage.includes("signing failed");
|
|
186
165
|
if (isSigningError && input.forceUnsignedOnFailure) {
|
|
187
166
|
logger.warning("Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.", { ...context, operation, initialError: initialErrorMessage });
|
|
188
|
-
// Construct command *without* -S flag
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
.replace(/\\/g, "\\\\")
|
|
192
|
-
.replace(/"/g, '\\"')
|
|
193
|
-
.replace(/`/g, "\\`")
|
|
194
|
-
.replace(/\$/g, "\\$");
|
|
195
|
-
};
|
|
196
|
-
const escapedMessage = escapeShellArg(input.message);
|
|
197
|
-
let unsignedCommand = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
|
|
198
|
-
if (input.allowEmpty)
|
|
199
|
-
unsignedCommand += " --allow-empty";
|
|
200
|
-
if (input.amend)
|
|
201
|
-
unsignedCommand += " --amend --no-edit";
|
|
202
|
-
if (input.author) {
|
|
203
|
-
const escapedAuthorName = escapeShellArg(input.author.name);
|
|
204
|
-
const escapedAuthorEmail = escapeShellArg(input.author.email);
|
|
205
|
-
unsignedCommand = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
|
|
206
|
-
// Re-append common flags if author block overwrote command
|
|
207
|
-
if (input.allowEmpty && !unsignedCommand.includes(" --allow-empty"))
|
|
208
|
-
unsignedCommand += " --allow-empty";
|
|
209
|
-
if (input.amend && !unsignedCommand.includes(" --amend"))
|
|
210
|
-
unsignedCommand += " --amend --no-edit";
|
|
211
|
-
}
|
|
212
|
-
logger.debug(`Executing unsigned fallback command: ${unsignedCommand}`, { ...context, operation });
|
|
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 });
|
|
213
170
|
try {
|
|
214
171
|
// Retry commit without signing
|
|
215
|
-
const fallbackResult = await
|
|
172
|
+
const fallbackResult = await execFileAsync("git", unsignedArgs);
|
|
216
173
|
stdout = fallbackResult.stdout;
|
|
217
174
|
stderr = fallbackResult.stderr;
|
|
218
175
|
// Add a note to the status message indicating signing was skipped
|
|
@@ -281,12 +238,19 @@ export async function commitGitChanges(input, context) {
|
|
|
281
238
|
if (commitHash) {
|
|
282
239
|
try {
|
|
283
240
|
// Get the list of files included in this specific commit
|
|
284
|
-
const
|
|
285
|
-
|
|
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(" ")}`, {
|
|
286
250
|
...context,
|
|
287
251
|
operation,
|
|
288
252
|
});
|
|
289
|
-
const { stdout: showStdout } = await
|
|
253
|
+
const { stdout: showStdout } = await execFileAsync("git", showArgs);
|
|
290
254
|
committedFiles = showStdout.trim().split("\n").filter(Boolean); // Split by newline, remove empty lines
|
|
291
255
|
logger.debug(`Retrieved committed files list for ${commitHash}`, {
|
|
292
256
|
...context,
|