@cyanheads/git-mcp-server 2.1.7 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/mcp-server/server.js +69 -228
- package/dist/mcp-server/tools/gitAdd/index.js +2 -4
- package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
- package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
- package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
- package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
- package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
- package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
- package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
- package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
- package/dist/mcp-server/tools/gitClean/index.js +3 -5
- package/dist/mcp-server/tools/gitClean/logic.js +44 -143
- package/dist/mcp-server/tools/gitClean/registration.js +52 -92
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
- package/dist/mcp-server/tools/gitClone/index.js +2 -4
- package/dist/mcp-server/tools/gitClone/logic.js +50 -171
- package/dist/mcp-server/tools/gitClone/registration.js +51 -42
- package/dist/mcp-server/tools/gitCommit/index.js +2 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
- package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
- package/dist/mcp-server/tools/gitDiff/index.js +2 -3
- package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
- package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
- package/dist/mcp-server/tools/gitFetch/index.js +2 -3
- package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
- package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
- package/dist/mcp-server/tools/gitInit/index.js +3 -5
- package/dist/mcp-server/tools/gitInit/logic.js +46 -152
- package/dist/mcp-server/tools/gitInit/registration.js +52 -104
- package/dist/mcp-server/tools/gitLog/index.js +2 -3
- package/dist/mcp-server/tools/gitLog/logic.js +75 -257
- package/dist/mcp-server/tools/gitLog/registration.js +54 -66
- package/dist/mcp-server/tools/gitMerge/index.js +3 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
- package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
- package/dist/mcp-server/tools/gitPull/index.js +2 -3
- package/dist/mcp-server/tools/gitPull/logic.js +48 -146
- package/dist/mcp-server/tools/gitPull/registration.js +53 -75
- package/dist/mcp-server/tools/gitPush/index.js +2 -3
- package/dist/mcp-server/tools/gitPush/logic.js +73 -181
- package/dist/mcp-server/tools/gitPush/registration.js +53 -75
- package/dist/mcp-server/tools/gitRebase/index.js +3 -5
- package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
- package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
- package/dist/mcp-server/tools/gitRemote/index.js +3 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
- package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
- package/dist/mcp-server/tools/gitReset/index.js +2 -3
- package/dist/mcp-server/tools/gitReset/logic.js +37 -121
- package/dist/mcp-server/tools/gitReset/registration.js +53 -60
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
- package/dist/mcp-server/tools/gitShow/index.js +3 -5
- package/dist/mcp-server/tools/gitShow/logic.js +33 -122
- package/dist/mcp-server/tools/gitShow/registration.js +52 -74
- package/dist/mcp-server/tools/gitStash/index.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +70 -214
- package/dist/mcp-server/tools/gitStash/registration.js +52 -77
- package/dist/mcp-server/tools/gitStatus/index.js +2 -4
- package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
- package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
- package/dist/mcp-server/tools/gitTag/index.js +3 -5
- package/dist/mcp-server/tools/gitTag/logic.js +66 -188
- package/dist/mcp-server/tools/gitTag/registration.js +54 -73
- package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
- package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
- package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- package/package.json +12 -12
|
@@ -1,25 +1,10 @@
|
|
|
1
|
-
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
2
|
-
import { ErrorHandler } from "../../../utils/index.js";
|
|
3
|
-
// Import utils from barrel (logger from ../utils/internal/logger.js)
|
|
4
|
-
import { logger } from "../../../utils/index.js";
|
|
5
|
-
// Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
|
|
6
|
-
import { requestContextService } from "../../../utils/index.js";
|
|
7
|
-
// Import the result type along with the function and input schema
|
|
8
|
-
import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
9
|
-
import { commitGitChanges, GitCommitInputSchema, } from "./logic.js";
|
|
10
|
-
let _getWorkingDirectory;
|
|
11
|
-
let _getSessionId;
|
|
12
1
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param getWdFn - Function to get the working directory for a session.
|
|
16
|
-
* @param getSidFn - Function to get the session ID from context.
|
|
2
|
+
* @fileoverview Handles registration and error handling for the git_commit tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitCommit/registration
|
|
17
4
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
logger.info("State accessors initialized for git_commit tool registration.");
|
|
22
|
-
}
|
|
5
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
7
|
+
import { commitGitChanges, GitCommitInputSchema, GitCommitOutputSchema, } from "./logic.js";
|
|
23
8
|
const TOOL_NAME = "git_commit";
|
|
24
9
|
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
10
|
|
|
@@ -45,63 +30,57 @@ Closes #123 (if applicable).
|
|
|
45
30
|
- Commit related changes logically. Use the optional \`filesToStage\` parameter to auto-stage specific files before committing.
|
|
46
31
|
- 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
32
|
/**
|
|
48
|
-
* Registers the git_commit tool with the MCP server.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @param
|
|
52
|
-
* @returns {Promise<void>}
|
|
53
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
33
|
+
* Registers the git_commit tool with the MCP server instance.
|
|
34
|
+
* @param server The MCP server instance.
|
|
35
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
36
|
+
* @param getSessionId Function to get the session ID from context.
|
|
54
37
|
*/
|
|
55
|
-
export const registerGitCommitTool = async (server) => {
|
|
56
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
57
|
-
throw new Error("State accessors for git_commit must be initialized before registration.");
|
|
58
|
-
}
|
|
38
|
+
export const registerGitCommitTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
59
39
|
const operation = "registerGitCommitTool";
|
|
60
40
|
const context = requestContextService.createRequestContext({ operation });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
41
|
+
server.registerTool(TOOL_NAME, {
|
|
42
|
+
title: "Git Commit",
|
|
43
|
+
description: TOOL_DESCRIPTION,
|
|
44
|
+
inputSchema: GitCommitInputSchema.shape,
|
|
45
|
+
outputSchema: GitCommitOutputSchema.shape,
|
|
46
|
+
annotations: {
|
|
47
|
+
readOnlyHint: false,
|
|
48
|
+
destructiveHint: false,
|
|
49
|
+
idempotentHint: false, // Committing is not idempotent
|
|
50
|
+
openWorldHint: false,
|
|
51
|
+
},
|
|
52
|
+
}, async (params, callContext) => {
|
|
53
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
54
|
+
toolName: TOOL_NAME,
|
|
55
|
+
parentContext: callContext,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
const sessionId = getSessionId(handlerContext);
|
|
59
|
+
const result = await commitGitChanges(params, {
|
|
60
|
+
...handlerContext,
|
|
61
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
68
62
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
const logicContext = {
|
|
74
|
-
...requestContext,
|
|
75
|
-
sessionId: sessionId,
|
|
76
|
-
getWorkingDirectory: getWorkingDirectoryForSession,
|
|
63
|
+
return {
|
|
64
|
+
structuredContent: result,
|
|
65
|
+
content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
|
|
77
66
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Stringify the JSON object for the response content
|
|
86
|
-
text: JSON.stringify(commitResult, null, 2), // Pretty-print JSON
|
|
87
|
-
contentType: "application/json",
|
|
88
|
-
};
|
|
89
|
-
// Log based on the success flag in the result
|
|
90
|
-
if (commitResult.success) {
|
|
91
|
-
logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, logicContext);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
logger.info(`Tool ${TOOL_NAME} completed with non-fatal condition (e.g., nothing to commit), returning JSON`, logicContext);
|
|
95
|
-
}
|
|
96
|
-
// Even if success is false (e.g., nothing to commit), it's not a tool execution error
|
|
97
|
-
return { content: [resultContent] };
|
|
98
|
-
}, {
|
|
99
|
-
operation: toolOperation,
|
|
100
|
-
context: logicContext,
|
|
101
|
-
input: validatedArgs,
|
|
102
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR,
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
|
|
70
|
+
const handledError = ErrorHandler.handleError(error, {
|
|
71
|
+
operation: `tool:${TOOL_NAME}`,
|
|
72
|
+
context: handlerContext,
|
|
73
|
+
input: params,
|
|
103
74
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
75
|
+
const mcpError = handledError instanceof McpError
|
|
76
|
+
? handledError
|
|
77
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred.", { originalError: handledError });
|
|
78
|
+
return {
|
|
79
|
+
isError: true,
|
|
80
|
+
content: [{ type: "text", text: mcpError.message }],
|
|
81
|
+
structuredContent: mcpError.details,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
|
|
107
86
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Barrel file for the gitDiff tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitDiff/index
|
|
3
4
|
*/
|
|
4
|
-
export { registerGitDiffTool
|
|
5
|
-
// Export types if needed elsewhere, e.g.:
|
|
6
|
-
// export type { GitDiffInput, GitDiffResult } from './logic.js';
|
|
5
|
+
export { registerGitDiffTool } from "./registration.js";
|
|
@@ -1,279 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_diff tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitDiff/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
// Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
|
|
7
|
-
import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
|
|
8
|
-
// Import utils from barrel (sanitization from ../utils/security/sanitization.js)
|
|
9
|
-
import { sanitization } from "../../../utils/index.js";
|
|
8
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
9
|
+
import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
commit2: z
|
|
24
|
-
.string()
|
|
25
|
-
.optional()
|
|
26
|
-
.describe("Second commit, branch, or ref for comparison. If omitted, compares commit1 against the working tree or index."),
|
|
27
|
-
staged: z
|
|
28
|
-
.boolean()
|
|
29
|
-
.optional()
|
|
30
|
-
.default(false)
|
|
31
|
-
.describe("Show diff of changes staged for the next commit (compares index against HEAD). Overrides commit1/commit2 if true."),
|
|
32
|
-
file: z
|
|
33
|
-
.string()
|
|
34
|
-
.optional()
|
|
35
|
-
.describe("Limit the diff output to a specific file path."),
|
|
36
|
-
includeUntracked: z
|
|
37
|
-
.boolean()
|
|
38
|
-
.optional()
|
|
39
|
-
.default(false)
|
|
40
|
-
.describe("Include untracked files in the diff output (shows their full content as new files). This is a non-standard extension."),
|
|
41
|
-
// Add options like --name-only, --stat, context lines (-U<n>) if needed
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
|
+
export const GitDiffBaseSchema = z.object({
|
|
13
|
+
path: z.string().default(".").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."),
|
|
14
|
+
commit1: z.string().optional().describe("First commit, branch, or ref for comparison."),
|
|
15
|
+
commit2: z.string().optional().describe("Second commit, branch, or ref for comparison."),
|
|
16
|
+
staged: z.boolean().default(false).describe("Show diff of changes staged for the next commit."),
|
|
17
|
+
file: z.string().optional().describe("Limit the diff output to a specific file path."),
|
|
18
|
+
includeUntracked: z.boolean().default(false).describe("Include untracked files in the diff output."),
|
|
19
|
+
});
|
|
20
|
+
export const GitDiffInputSchema = GitDiffBaseSchema.refine(data => !(data.staged && (data.commit1 || data.commit2)), {
|
|
21
|
+
message: "Cannot use 'staged' option with specific commit references.",
|
|
22
|
+
path: ["staged"],
|
|
42
23
|
});
|
|
43
|
-
//
|
|
44
|
-
export const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
message: "
|
|
48
|
-
path: ["staged", "commit1", "commit2"], // Indicate related fields
|
|
24
|
+
// 2. DEFINE the Zod response schema.
|
|
25
|
+
export const GitDiffOutputSchema = z.object({
|
|
26
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
27
|
+
diff: z.string().describe("The diff output. Will be 'No changes found.' if there are no differences."),
|
|
28
|
+
message: z.string().describe("A summary message of the result."),
|
|
49
29
|
});
|
|
30
|
+
async function getUntrackedFilesDiff(targetPath, context) {
|
|
31
|
+
const { stdout } = await execFileAsync("git", ["-C", targetPath, "ls-files", "--others", "--exclude-standard"]);
|
|
32
|
+
const untrackedFiles = stdout.trim().split("\n").filter(Boolean);
|
|
33
|
+
if (untrackedFiles.length === 0)
|
|
34
|
+
return "";
|
|
35
|
+
let diffs = "";
|
|
36
|
+
for (const file of untrackedFiles) {
|
|
37
|
+
try {
|
|
38
|
+
const { stdout: diffOut } = await execFileAsync("git", ["-C", targetPath, "diff", "--no-index", "/dev/null", file]);
|
|
39
|
+
diffs += diffOut;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.stdout)
|
|
43
|
+
diffs += error.stdout;
|
|
44
|
+
else
|
|
45
|
+
logger.warning(`Failed to diff untracked file: ${file}`, { ...context, error: error.message });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return diffs;
|
|
49
|
+
}
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* @param {GitDiffInput} input - The validated input object.
|
|
54
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
55
|
-
* @returns {Promise<GitDiffResult>} A promise that resolves with the structured diff result.
|
|
56
|
-
* @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
|
|
51
|
+
* 4. IMPLEMENT the core logic function.
|
|
52
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
57
53
|
*/
|
|
58
|
-
export async function diffGitChanges(
|
|
54
|
+
export async function diffGitChanges(params, context) {
|
|
59
55
|
const operation = "diffGitChanges";
|
|
60
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (input.path && input.path !== ".") {
|
|
65
|
-
targetPath = input.path;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
const workingDir = context.getWorkingDirectory();
|
|
69
|
-
if (!workingDir) {
|
|
70
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
|
|
71
|
-
}
|
|
72
|
-
targetPath = workingDir;
|
|
73
|
-
}
|
|
74
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
75
|
-
allowAbsolute: true,
|
|
76
|
-
}).sanitizedPath;
|
|
77
|
-
logger.debug("Sanitized path", {
|
|
78
|
-
...context,
|
|
79
|
-
operation,
|
|
80
|
-
sanitizedPath: targetPath,
|
|
81
|
-
});
|
|
56
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
57
|
+
const workingDir = context.getWorkingDirectory();
|
|
58
|
+
if (params.path === "." && !workingDir) {
|
|
59
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
82
60
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
61
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
62
|
+
const args = ["-C", targetPath, "diff"];
|
|
63
|
+
if (params.staged) {
|
|
64
|
+
args.push("--staged");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (params.commit1)
|
|
68
|
+
args.push(params.commit1);
|
|
69
|
+
if (params.commit2)
|
|
70
|
+
args.push(params.commit2);
|
|
92
71
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const safeCommit2 = input.commit2?.replace(/[`$&;*()|<>]/g, "");
|
|
96
|
-
const safeFile = input.file?.replace(/[`$&;*()|<>]/g, "");
|
|
97
|
-
let untrackedFilesDiff = "";
|
|
98
|
-
let untrackedFilesCount = 0;
|
|
72
|
+
if (params.file)
|
|
73
|
+
args.push("--", params.file);
|
|
99
74
|
try {
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (safeCommit1) {
|
|
108
|
-
standardDiffArgs.push(safeCommit1);
|
|
75
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
76
|
+
const { stdout } = await execFileAsync("git", args, { maxBuffer: 1024 * 1024 * 20 });
|
|
77
|
+
let combinedDiff = stdout;
|
|
78
|
+
if (params.includeUntracked) {
|
|
79
|
+
const untrackedDiff = await getUntrackedFilesDiff(targetPath, context);
|
|
80
|
+
if (untrackedDiff) {
|
|
81
|
+
combinedDiff += (combinedDiff ? "\n" : "") + untrackedDiff;
|
|
109
82
|
}
|
|
110
|
-
if (safeCommit2) {
|
|
111
|
-
standardDiffArgs.push(safeCommit2);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// Add file path limiter if provided for standard diff
|
|
115
|
-
// Note: `input.file` will not apply to the untracked files part unless we explicitly filter them.
|
|
116
|
-
// For simplicity, `includeUntracked` will show all untracked files if `input.file` is also set.
|
|
117
|
-
if (safeFile) {
|
|
118
|
-
standardDiffArgs.push("--", safeFile); // Use '--' to separate paths from revisions
|
|
119
|
-
}
|
|
120
|
-
logger.debug(`Executing standard diff command: git ${standardDiffArgs.join(" ")}`, {
|
|
121
|
-
...context,
|
|
122
|
-
operation,
|
|
123
|
-
});
|
|
124
|
-
const { stdout: standardStdout, stderr: standardStderr } = await execFileAsync("git", standardDiffArgs, {
|
|
125
|
-
maxBuffer: 1024 * 1024 * 20,
|
|
126
|
-
});
|
|
127
|
-
if (standardStderr) {
|
|
128
|
-
logger.warning(`Git diff (standard) stderr: ${standardStderr}`, {
|
|
129
|
-
...context,
|
|
130
|
-
operation,
|
|
131
|
-
});
|
|
132
83
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (input.includeUntracked) {
|
|
136
|
-
logger.debug("Including untracked files.", { ...context, operation });
|
|
137
|
-
const listUntrackedArgs = [
|
|
138
|
-
"-C",
|
|
139
|
-
targetPath,
|
|
140
|
-
"ls-files",
|
|
141
|
-
"--others",
|
|
142
|
-
"--exclude-standard",
|
|
143
|
-
];
|
|
144
|
-
try {
|
|
145
|
-
const { stdout: untrackedFilesStdOut } = await execFileAsync("git", listUntrackedArgs);
|
|
146
|
-
const untrackedFiles = untrackedFilesStdOut
|
|
147
|
-
.trim()
|
|
148
|
-
.split("\n")
|
|
149
|
-
.filter((f) => f); // Filter out empty lines
|
|
150
|
-
if (untrackedFiles.length > 0) {
|
|
151
|
-
logger.info(`Found ${untrackedFiles.length} untracked files.`, {
|
|
152
|
-
...context,
|
|
153
|
-
operation,
|
|
154
|
-
untrackedFiles,
|
|
155
|
-
});
|
|
156
|
-
let individualUntrackedDiffs = "";
|
|
157
|
-
for (const untrackedFile of untrackedFiles) {
|
|
158
|
-
// Sanitize each untracked file path before using in command
|
|
159
|
-
const safeUntrackedFile = untrackedFile.replace(/[`$&;*()|<>]/g, "");
|
|
160
|
-
// Skip if file path becomes empty after sanitization (unlikely but safe)
|
|
161
|
-
if (!safeUntrackedFile)
|
|
162
|
-
continue;
|
|
163
|
-
const untrackedDiffArgs = [
|
|
164
|
-
"-C",
|
|
165
|
-
targetPath,
|
|
166
|
-
"diff",
|
|
167
|
-
"--no-index",
|
|
168
|
-
"/dev/null",
|
|
169
|
-
safeUntrackedFile,
|
|
170
|
-
];
|
|
171
|
-
logger.debug(`Executing diff for untracked file: git ${untrackedDiffArgs.join(" ")}`, { ...context, operation, file: safeUntrackedFile });
|
|
172
|
-
try {
|
|
173
|
-
const { stdout: untrackedFileDiffOut } = await execFileAsync("git", untrackedDiffArgs);
|
|
174
|
-
individualUntrackedDiffs += untrackedFileDiffOut;
|
|
175
|
-
untrackedFilesCount++;
|
|
176
|
-
}
|
|
177
|
-
catch (untrackedError) {
|
|
178
|
-
// For `git diff --no-index`, a non-zero exit code (usually 1) means differences were found.
|
|
179
|
-
// The actual diff output will be in untrackedError.stdout.
|
|
180
|
-
if (untrackedError.stdout) {
|
|
181
|
-
individualUntrackedDiffs += untrackedError.stdout;
|
|
182
|
-
untrackedFilesCount++;
|
|
183
|
-
// Log stderr if it exists, as it might contain actual error messages despite stdout having the diff
|
|
184
|
-
if (untrackedError.stderr) {
|
|
185
|
-
logger.warning(`Stderr while diffing untracked file ${safeUntrackedFile} (diff captured from stdout): ${untrackedError.stderr}`, { ...context, operation, file: safeUntrackedFile });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
// If stdout is empty, then it's a more genuine failure.
|
|
190
|
-
logger.warning(`Failed to diff untracked file: ${safeUntrackedFile}. Error: ${untrackedError.message}`, {
|
|
191
|
-
...context,
|
|
192
|
-
operation,
|
|
193
|
-
file: safeUntrackedFile,
|
|
194
|
-
errorDetails: {
|
|
195
|
-
stderr: untrackedError.stderr,
|
|
196
|
-
stdout: untrackedError.stdout,
|
|
197
|
-
code: untrackedError.code,
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
individualUntrackedDiffs += `\n--- Diff for untracked file ${safeUntrackedFile} failed: ${untrackedError.message}\n`;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (individualUntrackedDiffs) {
|
|
205
|
-
// Add a separator if standard diff also had output
|
|
206
|
-
if (combinedDiffOutput.trim()) {
|
|
207
|
-
combinedDiffOutput += "\n";
|
|
208
|
-
}
|
|
209
|
-
combinedDiffOutput += individualUntrackedDiffs;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
logger.info("No untracked files found.", { ...context, operation });
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
catch (lsFilesError) {
|
|
217
|
-
logger.warning(`Failed to list untracked files. Error: ${lsFilesError.message}`, {
|
|
218
|
-
...context,
|
|
219
|
-
operation,
|
|
220
|
-
error: lsFilesError.stderr || lsFilesError.stdout,
|
|
221
|
-
});
|
|
222
|
-
// Proceed without untracked files if listing fails
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const isNoChanges = combinedDiffOutput.trim() === "";
|
|
226
|
-
const finalDiffOutput = isNoChanges
|
|
227
|
-
? "No changes found."
|
|
228
|
-
: combinedDiffOutput;
|
|
229
|
-
let message = isNoChanges
|
|
230
|
-
? "No changes found."
|
|
231
|
-
: "Diff generated successfully.";
|
|
232
|
-
if (untrackedFilesCount > 0) {
|
|
233
|
-
message += ` Included ${untrackedFilesCount} untracked file(s).`;
|
|
234
|
-
}
|
|
235
|
-
logger.info(message, {
|
|
236
|
-
...context,
|
|
237
|
-
operation,
|
|
238
|
-
path: targetPath,
|
|
239
|
-
untrackedFilesProcessed: untrackedFilesCount,
|
|
240
|
-
});
|
|
84
|
+
const noChanges = combinedDiff.trim() === "";
|
|
85
|
+
const message = noChanges ? "No changes found." : `Diff generated successfully.${params.includeUntracked ? " Untracked files included." : ""}`;
|
|
241
86
|
return {
|
|
242
87
|
success: true,
|
|
243
|
-
diff:
|
|
88
|
+
diff: noChanges ? "No changes found." : combinedDiff,
|
|
244
89
|
message,
|
|
245
|
-
untrackedFilesProcessed: untrackedFilesCount,
|
|
246
90
|
};
|
|
247
91
|
}
|
|
248
92
|
catch (error) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
logger.error(`Failed to execute git diff operation`, {
|
|
252
|
-
...context,
|
|
253
|
-
operation,
|
|
254
|
-
path: targetPath,
|
|
255
|
-
error: error.message,
|
|
256
|
-
stderr: error.stderr,
|
|
257
|
-
stdout: error.stdout,
|
|
258
|
-
});
|
|
259
|
-
const errorMessage = error.stderr || error.stdout || error.message || "";
|
|
260
|
-
// Handle specific error cases
|
|
93
|
+
const errorMessage = error.stderr || error.message || "";
|
|
94
|
+
logger.error(`Failed to execute git diff command`, { ...context, operation, errorMessage });
|
|
261
95
|
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
262
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}
|
|
263
|
-
}
|
|
264
|
-
if (errorMessage.includes("fatal: bad object") ||
|
|
265
|
-
errorMessage.includes("unknown revision or path not in the working tree")) {
|
|
266
|
-
const invalidRef = input.commit1 || input.commit2 || input.file;
|
|
267
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference or file path specified: '${invalidRef}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
96
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`);
|
|
268
97
|
}
|
|
269
|
-
if (errorMessage.includes("
|
|
270
|
-
|
|
271
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided: '${ambiguousArg}'. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
98
|
+
if (errorMessage.includes("bad object") || errorMessage.includes("unknown revision")) {
|
|
99
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid commit reference or file path specified.`);
|
|
272
100
|
}
|
|
273
|
-
|
|
274
|
-
// However, standard 'git diff' usually exits 0 even with differences. Errors typically mean invalid input/repo state.
|
|
275
|
-
// We'll treat most exec errors as failures.
|
|
276
|
-
// Generic internal error for other failures
|
|
277
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git diff for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
|
|
101
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Git diff failed: ${errorMessage}`);
|
|
278
102
|
}
|
|
279
103
|
}
|