@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.
Files changed (79) hide show
  1. package/README.md +4 -4
  2. package/dist/mcp-server/server.js +69 -228
  3. package/dist/mcp-server/tools/gitAdd/index.js +2 -4
  4. package/dist/mcp-server/tools/gitAdd/logic.js +17 -74
  5. package/dist/mcp-server/tools/gitAdd/registration.js +38 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +118 -296
  8. package/dist/mcp-server/tools/gitBranch/registration.js +52 -66
  9. package/dist/mcp-server/tools/gitCheckout/index.js +2 -3
  10. package/dist/mcp-server/tools/gitCheckout/logic.js +47 -122
  11. package/dist/mcp-server/tools/gitCheckout/registration.js +53 -72
  12. package/dist/mcp-server/tools/gitCherryPick/index.js +3 -5
  13. package/dist/mcp-server/tools/gitCherryPick/logic.js +55 -162
  14. package/dist/mcp-server/tools/gitCherryPick/registration.js +52 -67
  15. package/dist/mcp-server/tools/gitClean/index.js +3 -5
  16. package/dist/mcp-server/tools/gitClean/logic.js +44 -143
  17. package/dist/mcp-server/tools/gitClean/registration.js +52 -92
  18. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +3 -5
  19. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +19 -26
  20. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +55 -73
  21. package/dist/mcp-server/tools/gitClone/index.js +2 -4
  22. package/dist/mcp-server/tools/gitClone/logic.js +50 -171
  23. package/dist/mcp-server/tools/gitClone/registration.js +51 -42
  24. package/dist/mcp-server/tools/gitCommit/index.js +2 -4
  25. package/dist/mcp-server/tools/gitCommit/logic.js +90 -295
  26. package/dist/mcp-server/tools/gitCommit/registration.js +52 -73
  27. package/dist/mcp-server/tools/gitDiff/index.js +2 -3
  28. package/dist/mcp-server/tools/gitDiff/logic.js +78 -254
  29. package/dist/mcp-server/tools/gitDiff/registration.js +53 -68
  30. package/dist/mcp-server/tools/gitFetch/index.js +2 -3
  31. package/dist/mcp-server/tools/gitFetch/logic.js +47 -129
  32. package/dist/mcp-server/tools/gitFetch/registration.js +54 -66
  33. package/dist/mcp-server/tools/gitInit/index.js +3 -5
  34. package/dist/mcp-server/tools/gitInit/logic.js +46 -152
  35. package/dist/mcp-server/tools/gitInit/registration.js +52 -104
  36. package/dist/mcp-server/tools/gitLog/index.js +2 -3
  37. package/dist/mcp-server/tools/gitLog/logic.js +75 -257
  38. package/dist/mcp-server/tools/gitLog/registration.js +54 -66
  39. package/dist/mcp-server/tools/gitMerge/index.js +3 -5
  40. package/dist/mcp-server/tools/gitMerge/logic.js +52 -179
  41. package/dist/mcp-server/tools/gitMerge/registration.js +52 -71
  42. package/dist/mcp-server/tools/gitPull/index.js +2 -3
  43. package/dist/mcp-server/tools/gitPull/logic.js +48 -146
  44. package/dist/mcp-server/tools/gitPull/registration.js +53 -75
  45. package/dist/mcp-server/tools/gitPush/index.js +2 -3
  46. package/dist/mcp-server/tools/gitPush/logic.js +73 -181
  47. package/dist/mcp-server/tools/gitPush/registration.js +53 -75
  48. package/dist/mcp-server/tools/gitRebase/index.js +3 -5
  49. package/dist/mcp-server/tools/gitRebase/logic.js +73 -202
  50. package/dist/mcp-server/tools/gitRebase/registration.js +52 -70
  51. package/dist/mcp-server/tools/gitRemote/index.js +3 -5
  52. package/dist/mcp-server/tools/gitRemote/logic.js +85 -193
  53. package/dist/mcp-server/tools/gitRemote/registration.js +52 -65
  54. package/dist/mcp-server/tools/gitReset/index.js +2 -3
  55. package/dist/mcp-server/tools/gitReset/logic.js +37 -121
  56. package/dist/mcp-server/tools/gitReset/registration.js +53 -60
  57. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +3 -5
  58. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +45 -133
  59. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +55 -85
  60. package/dist/mcp-server/tools/gitShow/index.js +3 -5
  61. package/dist/mcp-server/tools/gitShow/logic.js +33 -122
  62. package/dist/mcp-server/tools/gitShow/registration.js +52 -74
  63. package/dist/mcp-server/tools/gitStash/index.js +3 -5
  64. package/dist/mcp-server/tools/gitStash/logic.js +70 -214
  65. package/dist/mcp-server/tools/gitStash/registration.js +52 -77
  66. package/dist/mcp-server/tools/gitStatus/index.js +2 -4
  67. package/dist/mcp-server/tools/gitStatus/logic.js +82 -229
  68. package/dist/mcp-server/tools/gitStatus/registration.js +52 -66
  69. package/dist/mcp-server/tools/gitTag/index.js +3 -5
  70. package/dist/mcp-server/tools/gitTag/logic.js +66 -188
  71. package/dist/mcp-server/tools/gitTag/registration.js +54 -73
  72. package/dist/mcp-server/tools/gitWorktree/index.js +3 -5
  73. package/dist/mcp-server/tools/gitWorktree/logic.js +112 -322
  74. package/dist/mcp-server/tools/gitWorktree/registration.js +54 -58
  75. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +5 -3
  76. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +26 -38
  77. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
  78. package/dist/mcp-server/transports/httpTransport.js +2 -3
  79. 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
- * Initializes the state accessors needed by the tool registration.
14
- * This should be called once during server setup.
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
- export function initializeGitCommitStateAccessors(getWdFn, getSidFn) {
19
- _getWorkingDirectory = getWdFn;
20
- _getSessionId = getSidFn;
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
- * Uses the high-level server.tool() method for registration, schema validation, and routing.
50
- *
51
- * @param {McpServer} server - The McpServer instance to register the tool with.
52
- * @returns {Promise<void>}
53
- * @throws {Error} If registration fails or state accessors are not initialized.
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
- await ErrorHandler.tryCatch(async () => {
62
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCommitInputSchema.shape, // Provide the Zod schema shape
63
- async (validatedArgs, callContext) => {
64
- const toolOperation = "tool:git_commit";
65
- const requestContext = requestContextService.createRequestContext({
66
- operation: toolOperation,
67
- parentContext: callContext,
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
- const sessionId = _getSessionId(requestContext);
70
- const getWorkingDirectoryForSession = () => {
71
- return _getWorkingDirectory(sessionId);
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
- logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
79
- return await ErrorHandler.tryCatch(async () => {
80
- // Call the core logic function which now returns a GitCommitResult object
81
- const commitResult = await commitGitChanges(validatedArgs, logicContext);
82
- // Format the result as a JSON string within TextContent
83
- const resultContent = {
84
- type: "text",
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
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
106
- }, { operation, context, critical: true });
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, initializeGitDiffStateAccessors, } from "./registration.js";
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
- // Import utils from barrel (logger from ../utils/internal/logger.js)
5
- import { logger } from "../../../utils/index.js";
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
- // Define the base input schema without refinement
12
- const GitDiffInputBaseSchema = z.object({
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
- commit1: z
20
- .string()
21
- .optional()
22
- .describe("First commit, branch, or ref for comparison. If omitted, compares against the working tree or index (depending on 'staged')."),
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
- // Export the shape for registration
44
- export const GitDiffInputShape = GitDiffInputBaseSchema.shape;
45
- // Define the final schema with refinement for validation during execution
46
- export const GitDiffInputSchema = GitDiffInputBaseSchema.refine((data) => !(data.staged && (data.commit1 || data.commit2)), {
47
- message: "Cannot use 'staged' option with specific commit references (commit1 or commit2).",
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
- * Executes the 'git diff' command and returns the diff output.
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(input, context) {
54
+ export async function diffGitChanges(params, context) {
59
55
  const operation = "diffGitChanges";
60
- logger.debug(`Executing ${operation}`, { ...context, input });
61
- let targetPath;
62
- try {
63
- // Resolve and sanitize the target path
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
- catch (error) {
84
- logger.error("Path resolution or sanitization failed", {
85
- ...context,
86
- operation,
87
- error,
88
- });
89
- if (error instanceof McpError)
90
- throw error;
91
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
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
- // Basic sanitization for refs and file path
94
- const safeCommit1 = input.commit1?.replace(/[`$&;*()|<>]/g, "");
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
- // Construct the standard git diff command
101
- const standardDiffArgs = ["-C", targetPath, "diff"];
102
- if (input.staged) {
103
- standardDiffArgs.push("--staged"); // Or --cached
104
- }
105
- else {
106
- // Add commit references if not doing staged diff
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
- let combinedDiffOutput = standardStdout;
134
- // Handle untracked files if requested
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: finalDiffOutput,
88
+ diff: noChanges ? "No changes found." : combinedDiff,
244
89
  message,
245
- untrackedFilesProcessed: untrackedFilesCount,
246
90
  };
247
91
  }
248
92
  catch (error) {
249
- // This catch block now primarily handles errors from the *standard* diff command
250
- // or catastrophic failures before/after untracked file processing.
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}`, { context, operation, originalError: error });
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("ambiguous argument")) {
270
- const ambiguousArg = input.commit1 || input.commit2 || input.file;
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
- // If the command exits with an error but stdout has content, it might still be useful (e.g., diff with conflicts)
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
  }