@cyanheads/git-mcp-server 2.1.8 → 2.2.1

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 +40 -116
  5. package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
  6. package/dist/mcp-server/tools/gitBranch/index.js +3 -5
  7. package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
  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 -144
  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 +47 -173
  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 +45 -154
  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 +18 -32
  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 +47 -187
  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 +75 -310
  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 +72 -264
  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 +38 -136
  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 +40 -162
  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 +71 -266
  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 +45 -191
  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 +39 -156
  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 +65 -192
  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 +59 -207
  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 +76 -200
  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 +33 -133
  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 +39 -144
  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 +28 -133
  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 +59 -219
  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 +79 -236
  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 +57 -198
  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 +102 -328
  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 +25 -43
  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 +8 -8
@@ -1,112 +1,60 @@
1
- import path from "path";
2
- import { z } from "zod";
3
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
- import { ErrorHandler, logger, requestContextService, sanitization, } from "../../../utils/index.js";
5
- import { GitInitInputSchema, gitInitLogic, } from "./logic.js";
6
- const TOOL_NAME = "git_init";
7
- const TOOL_DESCRIPTION = "Initializes a new Git repository at the specified path. If path is relative or omitted, it resolves against the session working directory (if you have set the git_working_dir). Can optionally set the initial branch name and create a bare repository.";
8
- const RegistrationSchema = GitInitInputSchema.extend({
9
- path: z.string().min(1).optional().default("."),
10
- }).shape;
11
- // --- Module-level State Accessors ---
12
- // These will be populated by the initialize function called from server.ts
13
- let _getWorkingDirectory = () => undefined;
14
- let _getSessionIdFromContext = () => undefined;
15
1
  /**
16
- * Initializes state accessor functions for the git_init tool.
17
- * This function is called by the main server setup to provide the tool
18
- * with a way to access session-specific state (like the working directory)
19
- * without needing direct access to the server or transport layer internals.
20
- *
21
- * @param getWorkingDirectory - Function to retrieve the working directory for a given session ID.
22
- * @param getSessionIdFromContext - Function to extract the session ID from a tool's execution context.
2
+ * @fileoverview Handles registration and error handling for the git_init tool.
3
+ * @module src/mcp-server/tools/gitInit/registration
23
4
  */
24
- export function initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext) {
25
- _getWorkingDirectory = getWorkingDirectory;
26
- _getSessionIdFromContext = getSessionIdFromContext;
27
- logger.debug(`State accessors initialized for ${TOOL_NAME}`);
28
- }
5
+ import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
6
+ import { gitInitLogic, GitInitInputSchema, GitInitOutputSchema, } from "./logic.js";
7
+ const TOOL_NAME = "git_init";
8
+ const TOOL_DESCRIPTION = "Initializes a new Git repository at the specified path. If path is relative or omitted, it resolves against the session working directory (if you have set the git_working_dir). Can optionally set the initial branch name and create a bare repository.";
29
9
  /**
30
- * Registers the git_init tool with the MCP server.
31
- *
32
- * @param {McpServer} server - The McpServer instance to register the tool with.
33
- * @returns {Promise<void>}
34
- * @throws {Error} If registration fails.
10
+ * Registers the git_init tool with the MCP server instance.
11
+ * @param server The MCP server instance.
12
+ * @param getSessionId Function to get the session ID from context.
35
13
  */
36
- export const registerGitInitTool = async (server) => {
14
+ export const registerGitInitTool = async (server, getSessionId) => {
37
15
  const operation = "registerGitInitTool";
38
16
  const context = requestContextService.createRequestContext({ operation });
39
- await ErrorHandler.tryCatch(async () => {
40
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, RegistrationSchema, async (validatedArgs, callContext) => {
41
- // Removed explicit type for callContext
42
- const toolOperation = "tool:git_init";
43
- const requestContext = requestContextService.createRequestContext({
44
- operation: toolOperation,
45
- parentContext: callContext,
46
- });
47
- // Use the initialized accessor to get the session ID
48
- const sessionId = _getSessionIdFromContext(requestContext); // Pass the created context
49
- if (!sessionId && !path.isAbsolute(validatedArgs.path)) {
50
- // If path is relative, we NEED a session ID to resolve against a potential working dir
51
- logger.error("Session ID is missing in context, cannot resolve relative path", requestContext);
52
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Session context is unavailable for relative path resolution.", { context: requestContext, operation: toolOperation });
53
- }
54
- logger.info(`Executing tool: ${TOOL_NAME}`, requestContext);
55
- return await ErrorHandler.tryCatch(async () => {
56
- // Use the initialized accessor to get the working directory
57
- const sessionWorkingDirectory = _getWorkingDirectory(sessionId);
58
- const inputPath = validatedArgs.path;
59
- let resolvedPath;
60
- try {
61
- if (path.isAbsolute(inputPath)) {
62
- resolvedPath = sanitization.sanitizePath(inputPath, {
63
- allowAbsolute: true,
64
- }).sanitizedPath;
65
- logger.debug(`Using absolute path: ${resolvedPath}`, requestContext);
66
- }
67
- else if (sessionWorkingDirectory) {
68
- resolvedPath = sanitization.sanitizePath(path.resolve(sessionWorkingDirectory, inputPath), { allowAbsolute: true }).sanitizedPath;
69
- logger.debug(`Resolved relative path '${inputPath}' to absolute path: ${resolvedPath} using session CWD`, requestContext);
70
- }
71
- else {
72
- // This case should now only be hit if the path is relative AND there's no session CWD set.
73
- logger.error(`Relative path '${inputPath}' provided but no session working directory is set.`, requestContext);
74
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Relative path '${inputPath}' provided but no session working directory is set. Please provide an absolute path or set a working directory using git_set_working_dir.`, { context: requestContext, operation: toolOperation });
75
- }
76
- }
77
- catch (error) {
78
- logger.error("Path resolution or sanitization failed", {
79
- ...requestContext,
80
- operation: toolOperation,
81
- error,
82
- });
83
- if (error instanceof McpError)
84
- throw error;
85
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path processing: ${error instanceof Error ? error.message : String(error)}`, {
86
- context: requestContext,
87
- operation: toolOperation,
88
- originalError: error,
89
- });
90
- }
91
- const logicArgs = {
92
- ...validatedArgs,
93
- path: resolvedPath,
94
- };
95
- const initResult = await gitInitLogic(logicArgs, requestContext);
96
- const resultContent = {
97
- type: "text",
98
- text: JSON.stringify(initResult, null, 2), // Pretty-print JSON
99
- contentType: "application/json",
100
- };
101
- logger.info(`Tool ${TOOL_NAME} executed successfully, returning JSON`, requestContext);
102
- return { content: [resultContent] };
103
- }, {
104
- operation: toolOperation,
105
- context: requestContext,
106
- input: validatedArgs,
107
- errorCode: BaseErrorCode.INTERNAL_ERROR,
108
- });
17
+ server.registerTool(TOOL_NAME, {
18
+ title: "Git Initialize",
19
+ description: TOOL_DESCRIPTION,
20
+ inputSchema: GitInitInputSchema.shape,
21
+ outputSchema: GitInitOutputSchema.shape,
22
+ annotations: {
23
+ readOnlyHint: false,
24
+ destructiveHint: true, // Creates a .git directory
25
+ idempotentHint: true, // Re-initializing is idempotent
26
+ openWorldHint: false,
27
+ },
28
+ }, async (params, callContext) => {
29
+ const handlerContext = requestContextService.createRequestContext({
30
+ toolName: TOOL_NAME,
31
+ parentContext: callContext,
109
32
  });
110
- logger.info(`Tool registered: ${TOOL_NAME}`, context);
111
- }, { operation, context, critical: true });
33
+ try {
34
+ // The logic function now handles path resolution.
35
+ const result = await gitInitLogic(params, handlerContext);
36
+ return {
37
+ structuredContent: result,
38
+ content: [{ type: "text", text: `Success: ${JSON.stringify(result, null, 2)}` }],
39
+ };
40
+ }
41
+ catch (error) {
42
+ logger.error(`Error in ${TOOL_NAME} handler`, { error, ...handlerContext });
43
+ const mcpError = ErrorHandler.handleError(error, {
44
+ operation: `tool:${TOOL_NAME}`,
45
+ context: handlerContext,
46
+ input: params,
47
+ });
48
+ return {
49
+ isError: true,
50
+ content: [{ type: "text", text: `Error: ${mcpError.message}` }],
51
+ structuredContent: {
52
+ code: mcpError.code,
53
+ message: mcpError.message,
54
+ details: mcpError.details,
55
+ },
56
+ };
57
+ }
58
+ });
59
+ logger.info(`Tool '${TOOL_NAME}' registered successfully.`, context);
112
60
  };
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitLog tool.
3
+ * @module src/mcp-server/tools/gitLog/index
3
4
  */
4
- export { registerGitLogTool, initializeGitLogStateAccessors, } from "./registration.js";
5
- // Export types if needed elsewhere, e.g.:
6
- // export type { GitLogInput, GitLogResult } from './logic.js';
5
+ export { registerGitLogTool } from "./registration.js";
@@ -1,284 +1,89 @@
1
+ /**
2
+ * @fileoverview Defines the core logic, schemas, and types for the git_log tool.
3
+ * @module src/mcp-server/tools/gitLog/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 structure for a single commit entry
12
- export const CommitEntrySchema = z.object({
11
+ // 1. DEFINE the Zod input schema.
12
+ export const GitLogInputSchema = z.object({
13
+ path: z.string().default(".").describe("Path to the Git repository."),
14
+ maxCount: z.number().int().positive().optional().describe("Limit the number of commits to output."),
15
+ author: z.string().optional().describe("Limit commits to those by a specific author."),
16
+ since: z.string().optional().describe("Show commits more recent than a specific date (e.g., '2 weeks ago')."),
17
+ until: z.string().optional().describe("Show commits older than a specific date."),
18
+ branchOrFile: z.string().optional().describe("Show logs for a specific branch, tag, or file path."),
19
+ showSignature: z.boolean().default(false).describe("Show signature verification status for commits."),
20
+ });
21
+ // 2. DEFINE the Zod response schema.
22
+ const CommitEntrySchema = z.object({
13
23
  hash: z.string().describe("Full commit hash"),
14
24
  authorName: z.string().describe("Author's name"),
15
25
  authorEmail: z.string().email().describe("Author's email"),
16
- timestamp: z
17
- .number()
18
- .int()
19
- .positive()
20
- .describe("Commit timestamp (Unix epoch seconds)"),
26
+ timestamp: z.number().int().positive().describe("Commit timestamp (Unix epoch seconds)"),
21
27
  subject: z.string().describe("Commit subject line"),
22
- body: z.string().optional().describe("Commit body (optional)"),
28
+ body: z.string().optional().describe("Commit body"),
23
29
  });
24
- // Define the input schema for the git_log tool using Zod
25
- export const GitLogInputSchema = z.object({
26
- path: z
27
- .string()
28
- .min(1)
29
- .optional()
30
- .default(".")
31
- .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."),
32
- maxCount: z
33
- .number()
34
- .int()
35
- .positive()
36
- .optional()
37
- .describe("Limit the number of commits to output."),
38
- author: z
39
- .string()
40
- .optional()
41
- .describe("Limit commits to those matching the specified author pattern."),
42
- since: z
43
- .string()
44
- .optional()
45
- .describe("Show commits more recent than a specific date (e.g., '2 weeks ago', '2023-01-01')."),
46
- until: z
47
- .string()
48
- .optional()
49
- .describe("Show commits older than a specific date."),
50
- branchOrFile: z
51
- .string()
52
- .optional()
53
- .describe("Show logs for a specific branch (e.g., 'main'), tag, or file path (e.g., 'src/utils/logger.ts')."),
54
- showSignature: z
55
- .boolean()
56
- .optional()
57
- .default(false)
58
- .describe("Show signature verification status for commits. Returns raw output instead of parsed JSON."),
59
- // Note: We use a fixed pretty format for reliable parsing unless showSignature is true.
30
+ export const GitLogOutputSchema = z.object({
31
+ success: z.boolean().describe("Indicates if the command was successful."),
32
+ message: z.string().describe("A summary message of the result."),
33
+ commits: z.array(CommitEntrySchema).optional().describe("A list of commits."),
34
+ rawOutput: z.string().optional().describe("Raw output from the git log command, used when showSignature is true."),
60
35
  });
61
- // Delimiters for parsing the custom format
62
- const FIELD_SEP = "\x1f"; // Unit Separator
63
- const RECORD_SEP = "\x1e"; // Record Separator
64
- const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`; // %H=hash, %an=author name, %ae=author email, %at=timestamp, %s=subject, %b=body
36
+ const FIELD_SEP = "\x1f";
37
+ const RECORD_SEP = "\x1e";
38
+ const GIT_LOG_FORMAT = `--pretty=format:%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%at${FIELD_SEP}%s${FIELD_SEP}%b${RECORD_SEP}`;
65
39
  /**
66
- * Executes the 'git log' command with a specific format and returns structured JSON output.
67
- *
68
- * @param {GitLogInput} input - The validated input object.
69
- * @param {RequestContext} context - The request context for logging and error handling.
70
- * @returns {Promise<GitLogResult>} A promise that resolves with the structured log result (either flat or grouped).
71
- * @throws {McpError} Throws an McpError if path resolution, validation, or the git command fails unexpectedly.
40
+ * 4. IMPLEMENT the core logic function.
41
+ * @throws {McpError} If the logic encounters an unrecoverable issue.
72
42
  */
73
- export async function logGitHistory(input, context) {
74
- // Return type updated to the union
43
+ export async function logGitHistory(params, context) {
75
44
  const operation = "logGitHistory";
76
- logger.debug(`Executing ${operation}`, { ...context, input });
77
- let targetPath;
78
- try {
79
- // Resolve and sanitize the target path
80
- if (input.path && input.path !== ".") {
81
- targetPath = input.path;
82
- }
83
- else {
84
- const workingDir = context.getWorkingDirectory();
85
- if (!workingDir) {
86
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
87
- }
88
- targetPath = workingDir;
89
- }
90
- targetPath = sanitization.sanitizePath(targetPath, {
91
- allowAbsolute: true,
92
- }).sanitizedPath;
93
- logger.debug("Sanitized path", {
94
- ...context,
95
- operation,
96
- sanitizedPath: targetPath,
97
- });
45
+ logger.debug(`Executing ${operation}`, { ...context, params });
46
+ const workingDir = context.getWorkingDirectory();
47
+ if (params.path === "." && !workingDir) {
48
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
49
+ }
50
+ const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
51
+ const args = ["-C", targetPath, "log"];
52
+ if (params.showSignature) {
53
+ args.push("--show-signature");
98
54
  }
99
- catch (error) {
100
- logger.error("Path resolution or sanitization failed", {
101
- ...context,
102
- operation,
103
- error,
104
- });
105
- if (error instanceof McpError)
106
- throw error;
107
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
55
+ else {
56
+ args.push(GIT_LOG_FORMAT);
108
57
  }
109
- try {
110
- const args = ["-C", targetPath, "log"];
111
- let isRawOutput = false; // Flag to indicate if we should parse or return raw
112
- if (input.showSignature) {
113
- isRawOutput = true;
114
- args.push("--show-signature");
115
- logger.info("Show signature requested, returning raw output.", {
116
- ...context,
117
- operation,
118
- });
119
- }
120
- else {
121
- args.push(GIT_LOG_FORMAT);
122
- }
123
- if (input.maxCount) {
124
- args.push(`-n${input.maxCount}`);
125
- }
126
- if (input.author) {
127
- args.push(`--author=${input.author}`);
128
- }
129
- if (input.since) {
130
- args.push(`--since=${input.since}`);
131
- }
132
- if (input.until) {
133
- args.push(`--until=${input.until}`);
134
- }
135
- if (input.branchOrFile) {
136
- args.push(input.branchOrFile);
137
- }
138
- logger.debug(`Executing command: git ${args.join(" ")}`, {
139
- ...context,
140
- operation,
141
- });
142
- // Increase maxBuffer if logs can be large
143
- const { stdout, stderr } = await execFileAsync("git", args, {
144
- maxBuffer: 1024 * 1024 * 10,
145
- }); // 10MB buffer
146
- if (stderr) {
147
- // Log stderr as warning, as git log might sometimes use it for non-fatal info
148
- // Exception: If showing signature, stderr about allowedSignersFile is expected, treat as info
149
- if (isRawOutput &&
150
- stderr.includes("allowedSignersFile needs to be configured")) {
151
- logger.info(`Git log stderr (signature verification note): ${stderr.trim()}`, { ...context, operation });
152
- }
153
- else {
154
- logger.warning(`Git log stderr: ${stderr.trim()}`, {
155
- ...context,
156
- operation,
157
- });
158
- }
159
- }
160
- // If raw output was requested, return it directly in the message field, omitting commits
161
- if (isRawOutput) {
162
- const message = `Raw log output (showSignature=true):\n${stdout}`;
163
- logger.info(`${operation} completed successfully (raw output).`, {
164
- ...context,
165
- operation,
166
- path: targetPath,
167
- });
168
- // Return without the 'commits' or 'groupedCommits' field
169
- return { success: true, message: message };
170
- }
171
- // --- Parse the structured output into a flat list first ---
172
- const flatCommits = [];
173
- const commitRecords = stdout
174
- .split(RECORD_SEP)
175
- .filter((record) => record.trim() !== ""); // Split records and remove empty ones
176
- for (const record of commitRecords) {
177
- const trimmedRecord = record.trim(); // Trim leading/trailing whitespace (like newlines)
178
- if (!trimmedRecord)
179
- continue; // Skip empty records after trimming
180
- const fields = trimmedRecord.split(FIELD_SEP); // Split the trimmed record
181
- if (fields.length >= 5) {
182
- // Need at least hash, name, email, timestamp, subject
183
- try {
184
- const commitEntry = {
185
- hash: fields[0],
186
- authorName: fields[1],
187
- authorEmail: fields[2],
188
- timestamp: parseInt(fields[3], 10), // Unix timestamp
189
- subject: fields[4],
190
- body: fields[5] || undefined, // Body might be empty
191
- };
192
- // Validate parsed entry
193
- CommitEntrySchema.parse(commitEntry);
194
- flatCommits.push(commitEntry);
195
- }
196
- catch (parseError) {
197
- logger.warning(`Failed to parse commit record field`, {
198
- ...context,
199
- operation,
200
- fieldIndex: fields.findIndex((_, i) => i > 5),
201
- recordFragment: record.substring(0, 100),
202
- parseError,
203
- });
204
- // Decide whether to skip the commit or throw an error
205
- }
206
- }
207
- else {
208
- logger.warning(`Skipping commit record due to unexpected number of fields (${fields.length})`, { ...context, operation, recordFragment: record.substring(0, 100) });
209
- }
210
- }
211
- // --- Group the flat list by author ---
212
- const groupedCommitsMap = new Map();
213
- for (const commit of flatCommits) {
214
- const authorKey = `${commit.authorName} <${commit.authorEmail}>`;
215
- const groupedInfo = {
216
- hash: commit.hash,
217
- timestamp: commit.timestamp,
218
- subject: commit.subject,
219
- body: commit.body,
220
- };
221
- if (groupedCommitsMap.has(authorKey)) {
222
- groupedCommitsMap.get(authorKey).commits.push(groupedInfo);
223
- }
224
- else {
225
- groupedCommitsMap.set(authorKey, {
226
- authorName: commit.authorName,
227
- authorEmail: commit.authorEmail,
228
- commits: [groupedInfo],
229
- });
230
- }
231
- }
232
- const groupedCommits = Array.from(groupedCommitsMap.values());
233
- // --- Prepare final result ---
234
- const commitCount = flatCommits.length;
235
- const message = commitCount > 0
236
- ? `${commitCount} commit(s) found.`
237
- : "No commits found matching criteria.";
238
- logger.info(message, {
239
- ...context,
240
- operation,
241
- path: targetPath,
242
- commitCount: commitCount,
243
- authorGroupCount: groupedCommits.length,
244
- });
245
- return { success: true, groupedCommits, message }; // Return the grouped structure
58
+ if (params.maxCount)
59
+ args.push(`-n${params.maxCount}`);
60
+ if (params.author)
61
+ args.push(`--author=${params.author}`);
62
+ if (params.since)
63
+ args.push(`--since=${params.since}`);
64
+ if (params.until)
65
+ args.push(`--until=${params.until}`);
66
+ if (params.branchOrFile)
67
+ args.push(params.branchOrFile);
68
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
69
+ const { stdout, stderr } = await execFileAsync("git", args, { maxBuffer: 1024 * 1024 * 10 });
70
+ if (stderr && stderr.toLowerCase().includes("does not have any commits yet")) {
71
+ return { success: true, message: "Repository has no commits yet.", commits: [] };
246
72
  }
247
- catch (error) {
248
- logger.error(`Failed to execute git log command`, {
249
- ...context,
250
- operation,
251
- path: targetPath,
252
- error: error.message,
253
- stderr: error.stderr,
254
- stdout: error.stdout,
255
- });
256
- const errorMessage = error.stderr || error.stdout || error.message || "";
257
- // Handle specific error cases
258
- if (errorMessage.toLowerCase().includes("not a git repository")) {
259
- throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
260
- }
261
- if (errorMessage.includes("fatal: bad revision")) {
262
- throw new McpError(BaseErrorCode.NOT_FOUND, `Invalid branch, tag, or revision specified: '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
263
- }
264
- if (errorMessage.includes("fatal: ambiguous argument")) {
265
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Ambiguous argument provided (e.g., branch/tag/file conflict): '${input.branchOrFile}'. Error: ${errorMessage}`, { context, operation, originalError: error });
266
- }
267
- // Check if it's just that no commits were found
268
- if (errorMessage.includes("does not have any commits yet")) {
269
- logger.info("Repository has no commits yet.", {
270
- ...context,
271
- operation,
272
- path: targetPath,
273
- });
274
- // Return the grouped structure even for no commits
275
- return {
276
- success: true,
277
- groupedCommits: [],
278
- message: "Repository has no commits yet.",
279
- };
280
- }
281
- // Generic internal error for other failures
282
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to get git log for path: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error });
73
+ if (params.showSignature) {
74
+ return { success: true, message: "Raw log output with signature status.", rawOutput: stdout };
283
75
  }
76
+ const commitRecords = stdout.split(RECORD_SEP).filter(r => r.trim());
77
+ const commits = commitRecords.map(record => {
78
+ const fields = record.trim().split(FIELD_SEP);
79
+ return {
80
+ hash: fields[0],
81
+ authorName: fields[1],
82
+ authorEmail: fields[2],
83
+ timestamp: parseInt(fields[3], 10),
84
+ subject: fields[4],
85
+ body: fields[5] || undefined,
86
+ };
87
+ });
88
+ return { success: true, message: `Found ${commits.length} commit(s).`, commits };
284
89
  }