@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,19 +1,34 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
-
import { logger } from
|
|
5
|
+
import { logger } from "../../../utils/index.js";
|
|
6
6
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from
|
|
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
|
-
import { sanitization } from
|
|
9
|
+
import { sanitization } from "../../../utils/index.js";
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
// Define the input schema for the git_checkout tool using Zod
|
|
12
12
|
export const GitCheckoutInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
path: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.optional()
|
|
17
|
+
.default(".")
|
|
18
|
+
.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."),
|
|
19
|
+
branchOrPath: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.describe("The branch name (e.g., 'main'), commit hash, tag, or file path(s) (e.g., './src/file.ts') to checkout."),
|
|
23
|
+
newBranch: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Create a new branch named <new_branch> (e.g., 'feat/new-feature') and start it at <branchOrPath>."),
|
|
27
|
+
force: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.optional()
|
|
30
|
+
.default(false)
|
|
31
|
+
.describe("Force checkout even if there are uncommitted changes (use with caution, discards local changes)."),
|
|
17
32
|
// Add other relevant git checkout options as needed (e.g., --track, -b for new branch shorthand)
|
|
18
33
|
});
|
|
19
34
|
/**
|
|
@@ -26,12 +41,12 @@ export const GitCheckoutInputSchema = z.object({
|
|
|
26
41
|
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
27
42
|
*/
|
|
28
43
|
export async function checkoutGit(input, context) {
|
|
29
|
-
const operation =
|
|
44
|
+
const operation = "checkoutGit";
|
|
30
45
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
31
46
|
let targetPath;
|
|
32
47
|
try {
|
|
33
48
|
// Resolve and sanitize the target path
|
|
34
|
-
if (input.path && input.path !==
|
|
49
|
+
if (input.path && input.path !== ".") {
|
|
35
50
|
targetPath = input.path;
|
|
36
51
|
}
|
|
37
52
|
else {
|
|
@@ -41,170 +56,95 @@ export async function checkoutGit(input, context) {
|
|
|
41
56
|
}
|
|
42
57
|
targetPath = workingDir;
|
|
43
58
|
}
|
|
44
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
45
|
-
|
|
59
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
60
|
+
allowAbsolute: true,
|
|
61
|
+
}).sanitizedPath;
|
|
62
|
+
logger.debug("Sanitized path", {
|
|
63
|
+
...context,
|
|
64
|
+
operation,
|
|
65
|
+
sanitizedPath: targetPath,
|
|
66
|
+
});
|
|
46
67
|
}
|
|
47
68
|
catch (error) {
|
|
48
|
-
logger.error(
|
|
69
|
+
logger.error("Path resolution or sanitization failed", {
|
|
70
|
+
...context,
|
|
71
|
+
operation,
|
|
72
|
+
error,
|
|
73
|
+
});
|
|
49
74
|
if (error instanceof McpError)
|
|
50
75
|
throw error;
|
|
51
76
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
52
77
|
}
|
|
53
78
|
// Basic sanitization for branch/path argument
|
|
54
|
-
const safeBranchOrPath = input.branchOrPath.replace(/[`$&;*()|<>]/g,
|
|
79
|
+
const safeBranchOrPath = input.branchOrPath.replace(/[`$&;*()|<>]/g, ""); // Remove potentially dangerous characters
|
|
55
80
|
try {
|
|
56
81
|
// Construct the git checkout command
|
|
57
82
|
let command = `git -C "${targetPath}" checkout`;
|
|
58
83
|
if (input.force) {
|
|
59
|
-
command +=
|
|
84
|
+
command += " --force";
|
|
60
85
|
}
|
|
61
86
|
if (input.newBranch) {
|
|
62
|
-
const safeNewBranch = input.newBranch.replace(/[^a-zA-Z0-9_.\-/]/g,
|
|
87
|
+
const safeNewBranch = input.newBranch.replace(/[^a-zA-Z0-9_.\-/]/g, ""); // Sanitize new branch name
|
|
63
88
|
command += ` -b ${safeNewBranch}`;
|
|
64
89
|
}
|
|
65
90
|
command += ` ${safeBranchOrPath}`; // Add the target branch/path
|
|
66
91
|
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
67
92
|
// Execute command. Checkout often uses stderr for status messages.
|
|
68
93
|
const { stdout, stderr } = await execAsync(command);
|
|
69
|
-
|
|
70
|
-
logger.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let currentBranch
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
else if (stderr.includes('Already on')) {
|
|
99
|
-
const currentBranchMatch = stderr.match(/Already on ['"]?(.*?)['"]?/);
|
|
100
|
-
currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.branchOrPath; // Use matched or input
|
|
101
|
-
message = `Already on '${currentBranch}'.`;
|
|
102
|
-
}
|
|
103
|
-
else if (stderr.includes('Updated N path') || stdout.includes('Updated N path') || stderr.includes('Your branch is up to date with')) { // Checking out files or confirming current state
|
|
104
|
-
// Check if the input looks like file paths rather than a branch/commit
|
|
105
|
-
// This is heuristic - might need refinement if branch names look like paths
|
|
106
|
-
if (input.branchOrPath.includes('/') || input.branchOrPath.includes('.')) {
|
|
107
|
-
isFileCheckout = true;
|
|
108
|
-
message = `Restored or checked path(s): ${input.branchOrPath}`;
|
|
109
|
-
filesRestored = input.branchOrPath.split('\n').map(p => p.trim()).filter(p => p.length > 0);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
// Assume it was just confirming the current branch state
|
|
113
|
-
message = stderr.trim() || stdout.trim() || `Checked out ${input.branchOrPath}.`;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else if (stderr.includes('Previous HEAD position was') && stderr.includes('HEAD is now at')) { // Detached HEAD
|
|
117
|
-
message = `Checked out commit ${input.branchOrPath} (Detached HEAD state).`;
|
|
118
|
-
currentBranch = 'Detached HEAD';
|
|
119
|
-
isDetachedHead = true;
|
|
120
|
-
}
|
|
121
|
-
else if (stderr.includes('Note: switching to') || stderr.includes('Note: checking out')) { // Other detached HEAD variants
|
|
122
|
-
message = `Checked out ${input.branchOrPath} (Detached HEAD state).`;
|
|
123
|
-
currentBranch = 'Detached HEAD';
|
|
124
|
-
isDetachedHead = true;
|
|
125
|
-
}
|
|
126
|
-
else if (message.includes('fatal:')) {
|
|
127
|
-
success = false;
|
|
128
|
-
message = `Checkout failed: ${message}`;
|
|
129
|
-
logger.error(`Git checkout command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
|
|
130
|
-
}
|
|
131
|
-
else if (!message && !stdout && !stderr) {
|
|
132
|
-
message = 'Checkout command executed silently.'; // Assume success, will verify branch below
|
|
133
|
-
logger.info(message, { ...context, operation });
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// Some other message, treat as informational for now
|
|
137
|
-
message = stderr.trim() || stdout.trim();
|
|
138
|
-
logger.info(`Git checkout produced message: ${message}`, { ...context, operation });
|
|
139
|
-
}
|
|
140
|
-
// --- Get definitive current branch IF checkout was successful AND not file checkout/detached HEAD ---
|
|
141
|
-
if (success && !isFileCheckout && !isDetachedHead) {
|
|
142
|
-
try {
|
|
143
|
-
logger.debug('Attempting to get current branch via git branch --show-current', { ...context, operation });
|
|
144
|
-
const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
|
|
145
|
-
const definitiveCurrentBranch = statusResult.stdout.trim();
|
|
146
|
-
if (definitiveCurrentBranch) {
|
|
147
|
-
currentBranch = definitiveCurrentBranch;
|
|
148
|
-
logger.info(`Confirmed current branch: ${currentBranch}`, { ...context, operation });
|
|
149
|
-
// Refine message if it wasn't specific before
|
|
150
|
-
if (message.startsWith('Checkout command executed silently') || message.startsWith('Checked out ')) {
|
|
151
|
-
message = `Checked out '${currentBranch}'.`;
|
|
152
|
-
}
|
|
153
|
-
else if (message.startsWith('Already on') && !message.includes(`'${currentBranch}'`)) {
|
|
154
|
-
message = `Already on '${currentBranch}'.`; // Update if initial parse was wrong
|
|
155
|
-
}
|
|
156
|
-
else if (message.startsWith('Switched to branch') && !message.includes(`'${currentBranch}'`)) {
|
|
157
|
-
message = `Switched to branch '${currentBranch}'.`; // Update if initial parse was wrong
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
// Command succeeded but returned empty - might be detached HEAD after all?
|
|
162
|
-
logger.warning('git branch --show-current returned empty, possibly detached HEAD?', { ...context, operation });
|
|
163
|
-
// Keep potentially parsed 'Detached HEAD' or fallback to input if needed
|
|
164
|
-
currentBranch = currentBranch || 'Unknown (possibly detached)';
|
|
165
|
-
if (!message.includes('Detached HEAD'))
|
|
166
|
-
message += ' (Could not confirm branch name).';
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch (statusError) {
|
|
170
|
-
logger.warning('Could not determine current branch after checkout', { ...context, operation, error: statusError.message });
|
|
171
|
-
// Keep potentially parsed 'Detached HEAD' or fallback to input if needed
|
|
172
|
-
currentBranch = currentBranch || 'Unknown (error checking)';
|
|
173
|
-
if (!message.includes('Detached HEAD'))
|
|
174
|
-
message += ' (Error checking branch name).';
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
else if (success && isFileCheckout) {
|
|
178
|
-
// If it was a file checkout, still try to get the branch name for context
|
|
179
|
-
try {
|
|
180
|
-
const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
|
|
181
|
-
currentBranch = statusResult.stdout.trim() || 'Unknown (possibly detached)';
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
currentBranch = 'Unknown (error checking)';
|
|
185
|
-
}
|
|
186
|
-
logger.info(`Current branch after file checkout: ${currentBranch}`, { ...context, operation });
|
|
187
|
-
}
|
|
188
|
-
logger.info(`${operation} completed`, { ...context, operation, path: targetPath, success, message, currentBranch });
|
|
189
|
-
return { success, message, previousBranch, currentBranch, newBranchCreated, filesRestored };
|
|
94
|
+
const message = stderr.trim() || stdout.trim();
|
|
95
|
+
logger.debug(`Git checkout stdout: ${stdout}`, { ...context, operation });
|
|
96
|
+
if (stderr) {
|
|
97
|
+
logger.debug(`Git checkout stderr: ${stderr}`, { ...context, operation });
|
|
98
|
+
}
|
|
99
|
+
// Get the current branch name after the checkout operation
|
|
100
|
+
let currentBranch;
|
|
101
|
+
try {
|
|
102
|
+
const { stdout: branchStdout } = await execAsync(`git -C "${targetPath}" branch --show-current`);
|
|
103
|
+
currentBranch = branchStdout.trim();
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
// This can fail in detached HEAD state, which is not an error for checkout
|
|
107
|
+
currentBranch = "Detached HEAD";
|
|
108
|
+
}
|
|
109
|
+
const result = {
|
|
110
|
+
success: true,
|
|
111
|
+
message,
|
|
112
|
+
currentBranch,
|
|
113
|
+
newBranchCreated: !!input.newBranch,
|
|
114
|
+
};
|
|
115
|
+
logger.info("git checkout executed successfully", {
|
|
116
|
+
...context,
|
|
117
|
+
operation,
|
|
118
|
+
path: targetPath,
|
|
119
|
+
result,
|
|
120
|
+
});
|
|
121
|
+
return result;
|
|
190
122
|
}
|
|
191
123
|
catch (error) {
|
|
192
|
-
logger.error(`Failed to execute git checkout command`, {
|
|
193
|
-
|
|
124
|
+
logger.error(`Failed to execute git checkout command`, {
|
|
125
|
+
...context,
|
|
126
|
+
operation,
|
|
127
|
+
path: targetPath,
|
|
128
|
+
error: error.message,
|
|
129
|
+
stderr: error.stderr,
|
|
130
|
+
stdout: error.stdout,
|
|
131
|
+
});
|
|
132
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
194
133
|
// Handle specific error cases
|
|
195
|
-
if (errorMessage.toLowerCase().includes(
|
|
134
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
196
135
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
197
136
|
}
|
|
198
137
|
if (errorMessage.match(/pathspec '.*?' did not match any file\(s\) known to git/)) {
|
|
199
138
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Branch or pathspec not found: ${input.branchOrPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
200
139
|
}
|
|
201
|
-
if (errorMessage.includes(
|
|
140
|
+
if (errorMessage.includes("already exists")) {
|
|
141
|
+
// e.g., trying -b with existing branch name
|
|
202
142
|
throw new McpError(BaseErrorCode.CONFLICT, `Cannot create new branch '${input.newBranch}': it already exists. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
203
143
|
}
|
|
204
|
-
if (errorMessage.includes(
|
|
144
|
+
if (errorMessage.includes("Your local changes to the following files would be overwritten by checkout")) {
|
|
205
145
|
throw new McpError(BaseErrorCode.CONFLICT, `Checkout failed due to uncommitted local changes that would be overwritten. Please commit or stash them first, or use --force. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
206
146
|
}
|
|
207
|
-
if (errorMessage.includes(
|
|
147
|
+
if (errorMessage.includes("invalid reference")) {
|
|
208
148
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid branch name or reference: ${input.branchOrPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
209
149
|
}
|
|
210
150
|
// Generic internal error for other failures
|
|
@@ -1,11 +1,11 @@
|
|
|
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, RequestContext from ../utils/internal/requestContext.js)
|
|
6
|
-
import { BaseErrorCode } from
|
|
7
|
-
import { requestContextService } from
|
|
8
|
-
import { checkoutGit, GitCheckoutInputSchema } from
|
|
6
|
+
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
7
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
8
|
+
import { checkoutGit, GitCheckoutInputSchema, } from "./logic.js";
|
|
9
9
|
let _getWorkingDirectory;
|
|
10
10
|
let _getSessionId;
|
|
11
11
|
/**
|
|
@@ -17,9 +17,9 @@ let _getSessionId;
|
|
|
17
17
|
export function initializeGitCheckoutStateAccessors(getWdFn, getSidFn) {
|
|
18
18
|
_getWorkingDirectory = getWdFn;
|
|
19
19
|
_getSessionId = getSidFn;
|
|
20
|
-
logger.info(
|
|
20
|
+
logger.info("State accessors initialized for git_checkout tool registration.");
|
|
21
21
|
}
|
|
22
|
-
const TOOL_NAME =
|
|
22
|
+
const TOOL_NAME = "git_checkout";
|
|
23
23
|
const TOOL_DESCRIPTION = "Switches branches or restores working tree files. Can checkout branches, commits, tags, or specific file paths. Supports creating new branches and forcing checkout.";
|
|
24
24
|
/**
|
|
25
25
|
* Registers the git_checkout tool with the MCP server.
|
|
@@ -29,15 +29,18 @@ const TOOL_DESCRIPTION = "Switches branches or restores working tree files. Can
|
|
|
29
29
|
*/
|
|
30
30
|
export async function registerGitCheckoutTool(server) {
|
|
31
31
|
if (!_getWorkingDirectory || !_getSessionId) {
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error("State accessors for git_checkout must be initialized before registration.");
|
|
33
33
|
}
|
|
34
|
-
const operation =
|
|
34
|
+
const operation = "registerGitCheckoutTool";
|
|
35
35
|
const context = requestContextService.createRequestContext({ operation });
|
|
36
36
|
await ErrorHandler.tryCatch(async () => {
|
|
37
37
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCheckoutInputSchema.shape, // Provide the Zod schema shape
|
|
38
38
|
async (validatedArgs, callContext) => {
|
|
39
|
-
const toolOperation =
|
|
40
|
-
const requestContext = requestContextService.createRequestContext({
|
|
39
|
+
const toolOperation = "tool:git_checkout";
|
|
40
|
+
const requestContext = requestContextService.createRequestContext({
|
|
41
|
+
operation: toolOperation,
|
|
42
|
+
parentContext: callContext,
|
|
43
|
+
});
|
|
41
44
|
const sessionId = _getSessionId(requestContext);
|
|
42
45
|
const getWorkingDirectoryForSession = () => {
|
|
43
46
|
return _getWorkingDirectory(sessionId);
|
|
@@ -53,9 +56,9 @@ export async function registerGitCheckoutTool(server) {
|
|
|
53
56
|
const checkoutResult = await checkoutGit(validatedArgs, logicContext);
|
|
54
57
|
// Format the result as a JSON string within TextContent
|
|
55
58
|
const resultContent = {
|
|
56
|
-
type:
|
|
59
|
+
type: "text",
|
|
57
60
|
text: JSON.stringify(checkoutResult, null, 2), // Pretty-print JSON
|
|
58
|
-
contentType:
|
|
61
|
+
contentType: "application/json",
|
|
59
62
|
};
|
|
60
63
|
// Log based on the success flag in the result
|
|
61
64
|
if (checkoutResult.success) {
|
|
@@ -78,4 +81,3 @@ export async function registerGitCheckoutTool(server) {
|
|
|
78
81
|
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
79
82
|
}, { operation, context, critical: true });
|
|
80
83
|
}
|
|
81
|
-
;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* @fileoverview Barrel file for the git_cherry_pick tool.
|
|
3
3
|
* Exports the registration function and state accessor initialization function.
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitCherryPickTool, initializeGitCherryPickStateAccessors } from
|
|
5
|
+
export { registerGitCherryPickTool, initializeGitCherryPickStateAccessors, } from "./registration.js";
|
|
6
6
|
// Export types if needed elsewhere, e.g.:
|
|
7
7
|
// export type { GitCherryPickInput, GitCherryPickResult } from './logic.js';
|
|
@@ -1,21 +1,43 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
5
|
-
import { logger } from
|
|
5
|
+
import { logger } from "../../../utils/index.js";
|
|
6
6
|
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from
|
|
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
|
-
import { sanitization } from
|
|
9
|
+
import { sanitization } from "../../../utils/index.js";
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
// Define the input schema for the git_cherry-pick tool using Zod
|
|
12
12
|
export const GitCherryPickInputSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
path: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.optional()
|
|
17
|
+
.default(".")
|
|
18
|
+
.describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
|
|
19
|
+
commitRef: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.describe("The commit reference(s) to cherry-pick (e.g., 'hash1', 'hash1..hash3', 'branchName~3..branchName')."),
|
|
23
|
+
mainline: z
|
|
24
|
+
.number()
|
|
25
|
+
.int()
|
|
26
|
+
.min(1)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Specify the parent number (starting from 1) when cherry-picking a merge commit."),
|
|
29
|
+
strategy: z
|
|
30
|
+
.enum(["recursive", "resolve", "ours", "theirs", "octopus", "subtree"])
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Specifies a merge strategy *option* (passed via -X)."),
|
|
33
|
+
noCommit: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.default(false)
|
|
36
|
+
.describe("Apply the changes but do not create a commit."),
|
|
37
|
+
signoff: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.default(false)
|
|
40
|
+
.describe("Add a Signed-off-by line to the commit message."),
|
|
19
41
|
// Add options for conflict handling? (e.g., --continue, --abort, --skip) - Maybe separate tool or mode?
|
|
20
42
|
});
|
|
21
43
|
/**
|
|
@@ -27,31 +49,47 @@ export const GitCherryPickInputSchema = z.object({
|
|
|
27
49
|
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
28
50
|
*/
|
|
29
51
|
export async function gitCherryPickLogic(input, context) {
|
|
30
|
-
const operation =
|
|
52
|
+
const operation = "gitCherryPickLogic";
|
|
31
53
|
logger.debug(`Executing ${operation}`, { ...context, input });
|
|
32
54
|
let targetPath;
|
|
33
55
|
try {
|
|
34
56
|
// Resolve and sanitize the target path
|
|
35
57
|
const workingDir = context.getWorkingDirectory();
|
|
36
|
-
targetPath =
|
|
37
|
-
? input.path
|
|
38
|
-
|
|
39
|
-
if (targetPath === '.' && !workingDir) {
|
|
58
|
+
targetPath =
|
|
59
|
+
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
60
|
+
if (targetPath === "." && !workingDir) {
|
|
40
61
|
logger.warning("Executing git cherry-pick in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
41
62
|
targetPath = process.cwd();
|
|
42
63
|
}
|
|
43
|
-
else if (targetPath ===
|
|
64
|
+
else if (targetPath === "." && workingDir) {
|
|
44
65
|
targetPath = workingDir;
|
|
45
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
66
|
+
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
67
|
+
...context,
|
|
68
|
+
operation,
|
|
69
|
+
sessionId: context.sessionId,
|
|
70
|
+
});
|
|
46
71
|
}
|
|
47
72
|
else {
|
|
48
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
73
|
+
logger.debug(`Using provided path: ${targetPath}`, {
|
|
74
|
+
...context,
|
|
75
|
+
operation,
|
|
76
|
+
});
|
|
49
77
|
}
|
|
50
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
51
|
-
|
|
78
|
+
targetPath = sanitization.sanitizePath(targetPath, {
|
|
79
|
+
allowAbsolute: true,
|
|
80
|
+
}).sanitizedPath;
|
|
81
|
+
logger.debug("Sanitized path", {
|
|
82
|
+
...context,
|
|
83
|
+
operation,
|
|
84
|
+
sanitizedPath: targetPath,
|
|
85
|
+
});
|
|
52
86
|
}
|
|
53
87
|
catch (error) {
|
|
54
|
-
logger.error(
|
|
88
|
+
logger.error("Path resolution or sanitization failed", {
|
|
89
|
+
...context,
|
|
90
|
+
operation,
|
|
91
|
+
error,
|
|
92
|
+
});
|
|
55
93
|
if (error instanceof McpError)
|
|
56
94
|
throw error;
|
|
57
95
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
|
|
@@ -63,9 +101,9 @@ export async function gitCherryPickLogic(input, context) {
|
|
|
63
101
|
if (input.strategy)
|
|
64
102
|
command += ` -X${input.strategy}`; // Note: -X for strategy options
|
|
65
103
|
if (input.noCommit)
|
|
66
|
-
command +=
|
|
104
|
+
command += " --no-commit";
|
|
67
105
|
if (input.signoff)
|
|
68
|
-
command +=
|
|
106
|
+
command += " --signoff";
|
|
69
107
|
// Add the commit reference(s) - ensure it's treated as a single argument potentially containing special chars like '..'
|
|
70
108
|
command += ` "${input.commitRef.replace(/"/g, '\\"')}"`;
|
|
71
109
|
logger.debug(`Executing command: ${command}`, { ...context, operation });
|
|
@@ -77,42 +115,63 @@ export async function gitCherryPickLogic(input, context) {
|
|
|
77
115
|
const commitCreated = !input.noCommit && !conflicts;
|
|
78
116
|
const message = conflicts
|
|
79
117
|
? `Cherry-pick resulted in conflicts for commit(s) '${input.commitRef}'. Manual resolution required.`
|
|
80
|
-
: `Successfully cherry-picked commit(s) '${input.commitRef}'.` +
|
|
81
|
-
|
|
118
|
+
: `Successfully cherry-picked commit(s) '${input.commitRef}'.` +
|
|
119
|
+
(commitCreated
|
|
120
|
+
? " New commit created."
|
|
121
|
+
: input.noCommit
|
|
122
|
+
? " Changes staged."
|
|
123
|
+
: "");
|
|
124
|
+
logger.info("git cherry-pick executed successfully", {
|
|
125
|
+
...context,
|
|
126
|
+
operation,
|
|
127
|
+
path: targetPath,
|
|
128
|
+
result: { message, conflicts, commitCreated },
|
|
129
|
+
});
|
|
82
130
|
return { success: true, message, commitCreated, conflicts };
|
|
83
131
|
}
|
|
84
132
|
catch (cherryPickError) {
|
|
85
|
-
const errorMessage = cherryPickError.stderr ||
|
|
133
|
+
const errorMessage = cherryPickError.stderr ||
|
|
134
|
+
cherryPickError.stdout ||
|
|
135
|
+
cherryPickError.message ||
|
|
136
|
+
"";
|
|
86
137
|
if (/conflict/i.test(errorMessage)) {
|
|
87
138
|
logger.warning(`Cherry-pick failed due to conflicts for commit(s) '${input.commitRef}'.`, { ...context, operation, path: targetPath, error: errorMessage });
|
|
88
|
-
return {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
message: `Failed to cherry-pick commit(s) '${input.commitRef}' due to conflicts. Resolve conflicts manually and potentially use 'git cherry-pick --continue' or '--abort'.`,
|
|
142
|
+
error: errorMessage,
|
|
143
|
+
conflicts: true,
|
|
144
|
+
};
|
|
89
145
|
}
|
|
90
146
|
// Rethrow other errors to be caught by the outer try-catch
|
|
91
147
|
throw cherryPickError;
|
|
92
148
|
}
|
|
93
149
|
}
|
|
94
150
|
catch (error) {
|
|
95
|
-
const errorMessage = error.stderr || error.stdout || error.message ||
|
|
96
|
-
logger.error(`Failed to execute git cherry-pick command`, {
|
|
151
|
+
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
152
|
+
logger.error(`Failed to execute git cherry-pick command`, {
|
|
153
|
+
...context,
|
|
154
|
+
operation,
|
|
155
|
+
path: targetPath,
|
|
156
|
+
error: errorMessage,
|
|
157
|
+
stderr: error.stderr,
|
|
158
|
+
stdout: error.stdout,
|
|
159
|
+
});
|
|
97
160
|
// Specific error handling
|
|
98
|
-
if (errorMessage.toLowerCase().includes(
|
|
161
|
+
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
99
162
|
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
100
163
|
}
|
|
101
164
|
if (/bad revision/i.test(errorMessage)) {
|
|
102
|
-
|
|
165
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to cherry-pick: Invalid commit reference '${input.commitRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
103
166
|
}
|
|
104
167
|
if (/after resolving the conflicts/i.test(errorMessage)) {
|
|
105
168
|
// This might indicate a previous conflict state
|
|
106
|
-
|
|
169
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to cherry-pick: Unresolved conflicts from a previous operation exist. Resolve conflicts and use 'git cherry-pick --continue' or '--abort'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
107
170
|
}
|
|
108
171
|
if (/your local changes would be overwritten/i.test(errorMessage)) {
|
|
109
|
-
|
|
172
|
+
throw new McpError(BaseErrorCode.CONFLICT, `Failed to cherry-pick: Your local changes to tracked files would be overwritten. Please commit or stash them. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
110
173
|
}
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
success: false,
|
|
114
|
-
message: `Git cherry-pick failed for path: ${targetPath}.`,
|
|
115
|
-
error: errorMessage
|
|
116
|
-
};
|
|
174
|
+
// Throw a generic McpError for other failures
|
|
175
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git cherry-pick failed for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
117
176
|
}
|
|
118
177
|
}
|