@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.
- 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 +40 -116
- package/dist/mcp-server/tools/gitAdd/registration.js +39 -59
- package/dist/mcp-server/tools/gitBranch/index.js +3 -5
- package/dist/mcp-server/tools/gitBranch/logic.js +109 -304
- 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 -144
- 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 +47 -173
- 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 +45 -154
- 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 +18 -32
- 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 +47 -187
- 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 +75 -310
- 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 +72 -264
- 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 +38 -136
- 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 +40 -162
- 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 +71 -266
- 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 +45 -191
- 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 +39 -156
- 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 +65 -192
- 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 +59 -207
- 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 +76 -200
- 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 +33 -133
- 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 +39 -144
- 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 +28 -133
- 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 +59 -219
- 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 +79 -236
- 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 +57 -198
- 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 +102 -328
- 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 +25 -43
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +54 -70
- package/dist/mcp-server/transports/httpTransport.js +2 -3
- 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
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
export const
|
|
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
|
|
28
|
+
body: z.string().optional().describe("Commit body"),
|
|
23
29
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
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
|
-
*
|
|
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(
|
|
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,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
}
|