@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,78 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
// Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
|
|
4
|
-
import { ErrorHandler } 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"; // Import BaseErrorCode
|
|
9
|
-
import { addGitFiles, GitAddInputSchema, } from "./logic.js";
|
|
10
|
-
let _getWorkingDirectory;
|
|
11
|
-
let _getSessionId;
|
|
12
|
-
/**
|
|
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.
|
|
17
|
-
*/
|
|
18
|
-
export function initializeGitAddStateAccessors(getWdFn, getSidFn) {
|
|
19
|
-
_getWorkingDirectory = getWdFn;
|
|
20
|
-
_getSessionId = getSidFn;
|
|
21
|
-
logger.info("State accessors initialized for git_add tool registration.");
|
|
22
|
-
}
|
|
1
|
+
import { ErrorHandler, logger, requestContextService } from "../../../utils/index.js";
|
|
2
|
+
import { addGitFiles, GitAddInputSchema, GitAddOutputSchema, } from "./logic.js";
|
|
23
3
|
const TOOL_NAME = "git_add";
|
|
24
4
|
const TOOL_DESCRIPTION = "Stages changes in the Git repository for the next commit by adding file contents to the index (staging area). Can stage specific files/patterns or all changes (default: '.'). Returns the result as a JSON object.";
|
|
25
|
-
|
|
26
|
-
* Registers the git_add tool with the MCP server.
|
|
27
|
-
* Uses the high-level server.tool() method for registration, schema validation, and routing.
|
|
28
|
-
*
|
|
29
|
-
* @param {McpServer} server - The McpServer instance to register the tool with.
|
|
30
|
-
* @returns {Promise<void>}
|
|
31
|
-
* @throws {Error} If registration fails or state accessors are not initialized.
|
|
32
|
-
*/
|
|
33
|
-
export const registerGitAddTool = async (server) => {
|
|
34
|
-
if (!_getWorkingDirectory || !_getSessionId) {
|
|
35
|
-
throw new Error("State accessors for git_add must be initialized before registration.");
|
|
36
|
-
}
|
|
5
|
+
export const registerGitAddTool = async (server, getWorkingDirectory, getSessionId) => {
|
|
37
6
|
const operation = "registerGitAddTool";
|
|
38
7
|
const context = requestContextService.createRequestContext({ operation });
|
|
39
8
|
await ErrorHandler.tryCatch(async () => {
|
|
40
|
-
server.
|
|
41
|
-
|
|
9
|
+
server.registerTool(TOOL_NAME, {
|
|
10
|
+
title: "Git Add",
|
|
11
|
+
description: TOOL_DESCRIPTION,
|
|
12
|
+
inputSchema: GitAddInputSchema.shape,
|
|
13
|
+
outputSchema: GitAddOutputSchema.shape,
|
|
14
|
+
annotations: {
|
|
15
|
+
readOnlyHint: false,
|
|
16
|
+
destructiveHint: false,
|
|
17
|
+
idempotentHint: true,
|
|
18
|
+
openWorldHint: false,
|
|
19
|
+
},
|
|
20
|
+
}, async (validatedArgs, callContext) => {
|
|
42
21
|
const toolOperation = "tool:git_add";
|
|
43
22
|
const requestContext = requestContextService.createRequestContext({
|
|
44
23
|
operation: toolOperation,
|
|
45
24
|
parentContext: callContext,
|
|
46
25
|
});
|
|
47
|
-
const sessionId =
|
|
48
|
-
const getWorkingDirectoryForSession = () => {
|
|
49
|
-
return _getWorkingDirectory(sessionId);
|
|
50
|
-
};
|
|
26
|
+
const sessionId = getSessionId(requestContext);
|
|
51
27
|
const logicContext = {
|
|
52
28
|
...requestContext,
|
|
53
29
|
sessionId: sessionId,
|
|
54
|
-
getWorkingDirectory:
|
|
30
|
+
getWorkingDirectory: () => getWorkingDirectory(sessionId),
|
|
55
31
|
};
|
|
56
32
|
logger.info(`Executing tool: ${TOOL_NAME}`, logicContext);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
type: "text",
|
|
63
|
-
// Stringify the JSON object for the response content
|
|
64
|
-
text: JSON.stringify(addResult, null, 2), // Pretty-print JSON
|
|
65
|
-
contentType: "application/json",
|
|
33
|
+
try {
|
|
34
|
+
const result = await addGitFiles(validatedArgs, logicContext);
|
|
35
|
+
return {
|
|
36
|
+
structuredContent: result,
|
|
37
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
66
38
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const mcpError = ErrorHandler.handleError(error, {
|
|
42
|
+
operation: "gitAddToolHandler",
|
|
43
|
+
context: logicContext,
|
|
44
|
+
input: validatedArgs,
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
isError: true,
|
|
48
|
+
content: [{ type: "text", text: `Error: ${mcpError.message}` }],
|
|
49
|
+
structuredContent: {
|
|
50
|
+
code: mcpError.code,
|
|
51
|
+
message: mcpError.message,
|
|
52
|
+
details: mcpError.details,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
76
56
|
});
|
|
77
57
|
logger.info(`Tool registered: ${TOOL_NAME}`, context);
|
|
78
58
|
}, { operation, context, critical: true });
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Barrel file for the
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Barrel file for the gitBranch tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitBranch/index
|
|
4
4
|
*/
|
|
5
|
-
export { registerGitBranchTool
|
|
6
|
-
// Export types if needed elsewhere, e.g.:
|
|
7
|
-
// export type { GitBranchInput, GitBranchResult } from './logic.js';
|
|
5
|
+
export { registerGitBranchTool } from "./registration.js";
|
|
@@ -1,324 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the core logic, schemas, and types for the git_branch tool.
|
|
3
|
+
* @module src/mcp-server/tools/gitBranch/logic
|
|
4
|
+
*/
|
|
1
5
|
import { execFile } from "child_process";
|
|
2
6
|
import { promisify } from "util";
|
|
3
7
|
import { z } from "zod";
|
|
4
|
-
|
|
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 { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
9
|
+
import { logger, sanitization } from "../../../utils/index.js";
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
-
//
|
|
11
|
+
// 1. DEFINE the Zod input schema.
|
|
12
12
|
export const GitBranchBaseSchema = z.object({
|
|
13
|
-
path: z
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.describe("The branch operation to perform: 'list', 'create', 'delete', 'rename', 'show-current'."),
|
|
22
|
-
branchName: z
|
|
23
|
-
.string()
|
|
24
|
-
.min(1)
|
|
25
|
-
.optional()
|
|
26
|
-
.describe("The name of the branch (e.g., 'feat/new-login', 'main'). Required for 'create', 'delete', 'rename' modes."),
|
|
27
|
-
newBranchName: z
|
|
28
|
-
.string()
|
|
29
|
-
.min(1)
|
|
30
|
-
.optional()
|
|
31
|
-
.describe("The new name for the branch (e.g., 'fix/typo-in-readme'). Required for 'rename' mode."),
|
|
32
|
-
startPoint: z
|
|
33
|
-
.string()
|
|
34
|
-
.min(1)
|
|
35
|
-
.optional()
|
|
36
|
-
.describe("Optional commit hash, tag, or existing branch name (e.g., 'main', 'v1.0.0', 'commit-hash') to start the new branch from. Used only in 'create' mode. Defaults to HEAD."),
|
|
37
|
-
force: z
|
|
38
|
-
.boolean()
|
|
39
|
-
.default(false)
|
|
40
|
-
.describe("Force the operation. Use -D for delete, -M for rename, -f for create (if branch exists). Use with caution, as forcing operations can lead to data loss."),
|
|
41
|
-
all: z
|
|
42
|
-
.boolean()
|
|
43
|
-
.default(false)
|
|
44
|
-
.describe("List both local and remote-tracking branches. Used only in 'list' mode."),
|
|
45
|
-
remote: z
|
|
46
|
-
.boolean()
|
|
47
|
-
.default(false)
|
|
48
|
-
.describe("Act on remote-tracking branches. Used with 'list' (-r) or 'delete' (-r)."),
|
|
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
|
+
mode: z.enum(["list", "create", "delete", "rename", "show-current"]).describe("The branch operation to perform."),
|
|
15
|
+
branchName: z.string().optional().describe("The name of the branch for create, delete, or rename operations."),
|
|
16
|
+
newBranchName: z.string().optional().describe("The new name for the branch when renaming."),
|
|
17
|
+
startPoint: z.string().optional().describe("The starting point (commit, tag, or branch) for a new branch."),
|
|
18
|
+
force: z.boolean().default(false).describe("Force the operation (e.g., overwrite existing branch)."),
|
|
19
|
+
all: z.boolean().default(false).describe("List all branches (local and remote)."),
|
|
20
|
+
remote: z.boolean().default(false).describe("Act on remote-tracking branches."),
|
|
49
21
|
});
|
|
50
|
-
|
|
51
|
-
export const GitBranchInputSchema = GitBranchBaseSchema.refine((data) => !(data.mode === "create" && !data.branchName), {
|
|
22
|
+
export const GitBranchInputSchema = GitBranchBaseSchema.refine(data => !(data.mode === "create" && !data.branchName), {
|
|
52
23
|
message: "A 'branchName' is required for 'create' mode.",
|
|
53
24
|
path: ["branchName"],
|
|
54
|
-
})
|
|
55
|
-
.refine((data) => !(data.mode === "delete" && !data.branchName), {
|
|
25
|
+
}).refine(data => !(data.mode === "delete" && !data.branchName), {
|
|
56
26
|
message: "A 'branchName' is required for 'delete' mode.",
|
|
57
27
|
path: ["branchName"],
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
28
|
+
}).refine(data => !(data.mode === "rename" && (!data.branchName || !data.newBranchName)), {
|
|
29
|
+
message: "Both 'branchName' and 'newBranchName' are required for 'rename' mode.",
|
|
30
|
+
path: ["newBranchName"],
|
|
31
|
+
});
|
|
32
|
+
// 2. DEFINE the Zod response schema.
|
|
33
|
+
const BranchInfoSchema = z.object({
|
|
34
|
+
name: z.string(),
|
|
35
|
+
isCurrent: z.boolean(),
|
|
36
|
+
isRemote: z.boolean(),
|
|
37
|
+
commitHash: z.string().optional(),
|
|
38
|
+
commitSubject: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
export const GitBranchOutputSchema = z.object({
|
|
41
|
+
success: z.boolean().describe("Indicates if the command was successful."),
|
|
42
|
+
mode: z.string().describe("The mode of operation that was performed."),
|
|
43
|
+
message: z.string().describe("A summary message of the result."),
|
|
44
|
+
branches: z.array(BranchInfoSchema).optional().describe("A list of branches for the 'list' mode."),
|
|
45
|
+
currentBranch: z.string().nullable().optional().describe("The current branch name."),
|
|
62
46
|
});
|
|
63
47
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @param {GitBranchInput} input - The validated input object.
|
|
67
|
-
* @param {RequestContext} context - The request context for logging and error handling.
|
|
68
|
-
* @returns {Promise<GitBranchResult>} A promise that resolves with the structured result.
|
|
69
|
-
* @throws {McpError} Throws an McpError for path resolution/validation failures or unexpected errors.
|
|
48
|
+
* 4. IMPLEMENT the core logic function.
|
|
49
|
+
* @throws {McpError} If the logic encounters an unrecoverable issue.
|
|
70
50
|
*/
|
|
71
|
-
export async function gitBranchLogic(
|
|
72
|
-
const operation = `gitBranchLogic:${
|
|
73
|
-
logger.debug(`Executing ${operation}`, { ...context,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const workingDir = context.getWorkingDirectory();
|
|
78
|
-
targetPath =
|
|
79
|
-
input.path && input.path !== "." ? input.path : (workingDir ?? ".");
|
|
80
|
-
if (targetPath === "." && !workingDir) {
|
|
81
|
-
logger.warning("Executing git branch in server's CWD as no path provided and no session WD set.", { ...context, operation });
|
|
82
|
-
targetPath = process.cwd();
|
|
83
|
-
}
|
|
84
|
-
else if (targetPath === "." && workingDir) {
|
|
85
|
-
targetPath = workingDir;
|
|
86
|
-
logger.debug(`Using session working directory: ${targetPath}`, {
|
|
87
|
-
...context,
|
|
88
|
-
operation,
|
|
89
|
-
sessionId: context.sessionId,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
logger.debug(`Using provided path: ${targetPath}`, {
|
|
94
|
-
...context,
|
|
95
|
-
operation,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
targetPath = sanitization.sanitizePath(targetPath, {
|
|
99
|
-
allowAbsolute: true,
|
|
100
|
-
}).sanitizedPath;
|
|
101
|
-
logger.debug("Sanitized path", {
|
|
102
|
-
...context,
|
|
103
|
-
operation,
|
|
104
|
-
sanitizedPath: targetPath,
|
|
105
|
-
});
|
|
51
|
+
export async function gitBranchLogic(params, context) {
|
|
52
|
+
const operation = `gitBranchLogic:${params.mode}`;
|
|
53
|
+
logger.debug(`Executing ${operation}`, { ...context, params });
|
|
54
|
+
const workingDir = context.getWorkingDirectory();
|
|
55
|
+
if (params.path === "." && !workingDir) {
|
|
56
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
|
|
106
57
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
58
|
+
const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
|
|
59
|
+
const args = ["-C", targetPath];
|
|
60
|
+
switch (params.mode) {
|
|
61
|
+
case "list":
|
|
62
|
+
args.push("branch", "--list", "--no-color", "--verbose");
|
|
63
|
+
if (params.all)
|
|
64
|
+
args.push("-a");
|
|
65
|
+
else if (params.remote)
|
|
66
|
+
args.push("-r");
|
|
67
|
+
break;
|
|
68
|
+
case "create":
|
|
69
|
+
args.push("branch");
|
|
70
|
+
if (params.force)
|
|
71
|
+
args.push("-f");
|
|
72
|
+
args.push(params.branchName, params.startPoint || "");
|
|
73
|
+
break;
|
|
74
|
+
case "delete":
|
|
75
|
+
args.push("branch", params.force ? "-D" : "-d");
|
|
76
|
+
if (params.remote)
|
|
77
|
+
args.push("-r");
|
|
78
|
+
args.push(params.branchName);
|
|
79
|
+
break;
|
|
80
|
+
case "rename":
|
|
81
|
+
args.push("branch", params.force ? "-M" : "-m", params.branchName, params.newBranchName);
|
|
82
|
+
break;
|
|
83
|
+
case "show-current":
|
|
84
|
+
args.push("branch", "--show-current");
|
|
85
|
+
break;
|
|
116
86
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
case "list":
|
|
122
|
-
args = ["-C", targetPath, "branch", "--list", "--no-color"]; // Start with basic list
|
|
123
|
-
if (input.all) {
|
|
124
|
-
args.push("-a"); // Add -a if requested
|
|
125
|
-
}
|
|
126
|
-
else if (input.remote) {
|
|
127
|
-
args.push("-r"); // Add -r if requested (exclusive with -a)
|
|
128
|
-
}
|
|
129
|
-
args.push("--verbose"); // Add verbose for commit info
|
|
130
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
131
|
-
...context,
|
|
132
|
-
operation,
|
|
133
|
-
});
|
|
134
|
-
const { stdout: listStdout } = await execFileAsync("git", args);
|
|
135
|
-
const branches = listStdout
|
|
136
|
-
.trim()
|
|
137
|
-
.split("\n")
|
|
138
|
-
.filter((line) => line && !line.match(/^\s*->\s*/)) // Filter out HEAD pointer lines if any
|
|
139
|
-
.map((line) => {
|
|
140
|
-
const isCurrent = line.startsWith("* ");
|
|
141
|
-
const trimmedLine = line.replace(/^\*?\s+/, ""); // Remove leading '*' and spaces
|
|
142
|
-
// Determine isRemote based on the raw trimmed line BEFORE splitting
|
|
143
|
-
const isRemote = trimmedLine.startsWith("remotes/");
|
|
144
|
-
const parts = trimmedLine.split(/\s+/);
|
|
145
|
-
const name = parts[0]; // This might be 'remotes/origin/main' or just 'main'
|
|
146
|
-
const commitHash = parts[1] || undefined; // Verbose gives hash
|
|
147
|
-
const commitSubject = parts.slice(2).join(" ") || undefined; // Verbose gives subject
|
|
148
|
-
// Return the correct name (without 'remotes/' prefix if it was remote) and the isRemote flag
|
|
149
|
-
return {
|
|
150
|
-
name: isRemote ? name.split("/").slice(2).join("/") : name, // e.g., 'origin/main' or 'main'
|
|
151
|
-
isCurrent,
|
|
152
|
-
isRemote, // Use the flag determined before splitting
|
|
153
|
-
commitHash,
|
|
154
|
-
commitSubject,
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
const currentBranch = branches.find((b) => b.isCurrent)?.name;
|
|
158
|
-
result = { success: true, mode: "list", branches, currentBranch };
|
|
159
|
-
break;
|
|
160
|
-
case "create":
|
|
161
|
-
// branchName is validated by Zod refine
|
|
162
|
-
args = ["-C", targetPath, "branch"];
|
|
163
|
-
if (input.force) {
|
|
164
|
-
args.push("-f");
|
|
165
|
-
}
|
|
166
|
-
args.push(input.branchName); // branchName is guaranteed by refine
|
|
167
|
-
if (input.startPoint) {
|
|
168
|
-
args.push(input.startPoint);
|
|
169
|
-
}
|
|
170
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
171
|
-
...context,
|
|
172
|
-
operation,
|
|
173
|
-
});
|
|
174
|
-
await execFileAsync("git", args);
|
|
175
|
-
result = {
|
|
176
|
-
success: true,
|
|
177
|
-
mode: "create",
|
|
178
|
-
branchName: input.branchName,
|
|
179
|
-
message: `Branch '${input.branchName}' created successfully.`,
|
|
180
|
-
};
|
|
181
|
-
break;
|
|
182
|
-
case "delete":
|
|
183
|
-
// branchName is validated by Zod refine
|
|
184
|
-
args = ["-C", targetPath, "branch"];
|
|
185
|
-
if (input.remote) {
|
|
186
|
-
args.push("-r");
|
|
187
|
-
}
|
|
188
|
-
args.push(input.force ? "-D" : "-d");
|
|
189
|
-
args.push(input.branchName); // branchName is guaranteed by refine
|
|
190
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
191
|
-
...context,
|
|
192
|
-
operation,
|
|
193
|
-
});
|
|
194
|
-
const { stdout: deleteStdout } = await execFileAsync("git", args);
|
|
195
|
-
result = {
|
|
196
|
-
success: true,
|
|
197
|
-
mode: "delete",
|
|
198
|
-
branchName: input.branchName,
|
|
199
|
-
wasRemote: input.remote,
|
|
200
|
-
message: deleteStdout.trim() ||
|
|
201
|
-
`Branch '${input.branchName}' deleted successfully.`,
|
|
202
|
-
};
|
|
203
|
-
break;
|
|
204
|
-
case "rename":
|
|
205
|
-
// branchName and newBranchName validated by Zod refine
|
|
206
|
-
args = ["-C", targetPath, "branch"];
|
|
207
|
-
args.push(input.force ? "-M" : "-m");
|
|
208
|
-
args.push(input.branchName, input.newBranchName);
|
|
209
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
210
|
-
...context,
|
|
211
|
-
operation,
|
|
212
|
-
});
|
|
213
|
-
await execFileAsync("git", args);
|
|
214
|
-
result = {
|
|
215
|
-
success: true,
|
|
216
|
-
mode: "rename",
|
|
217
|
-
oldBranchName: input.branchName,
|
|
218
|
-
newBranchName: input.newBranchName,
|
|
219
|
-
message: `Branch '${input.branchName}' renamed to '${input.newBranchName}' successfully.`,
|
|
220
|
-
};
|
|
221
|
-
break;
|
|
222
|
-
case "show-current":
|
|
223
|
-
args = ["-C", targetPath, "branch", "--show-current"];
|
|
224
|
-
logger.debug(`Executing command: git ${args.join(" ")}`, {
|
|
225
|
-
...context,
|
|
226
|
-
operation,
|
|
227
|
-
});
|
|
228
|
-
try {
|
|
229
|
-
const { stdout: currentStdout } = await execFileAsync("git", args);
|
|
230
|
-
const currentBranchName = currentStdout.trim();
|
|
231
|
-
result = {
|
|
232
|
-
success: true,
|
|
233
|
-
mode: "show-current",
|
|
234
|
-
currentBranch: currentBranchName || null,
|
|
235
|
-
message: currentBranchName
|
|
236
|
-
? `Current branch is '${currentBranchName}'.`
|
|
237
|
-
: "Currently in detached HEAD state.",
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
catch (showError) {
|
|
241
|
-
// Handle detached HEAD state specifically if command fails
|
|
242
|
-
if (showError.stderr?.includes("HEAD detached")) {
|
|
243
|
-
result = {
|
|
244
|
-
success: true,
|
|
245
|
-
mode: "show-current",
|
|
246
|
-
currentBranch: null,
|
|
247
|
-
message: "Currently in detached HEAD state.",
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
throw showError; // Re-throw other errors
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
break;
|
|
255
|
-
default:
|
|
256
|
-
// Should not happen due to Zod validation
|
|
257
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
|
|
258
|
-
}
|
|
259
|
-
logger.info(`git branch ${input.mode} executed successfully`, {
|
|
260
|
-
...context,
|
|
261
|
-
operation,
|
|
262
|
-
path: targetPath,
|
|
263
|
-
result,
|
|
264
|
-
});
|
|
265
|
-
return result;
|
|
87
|
+
logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
|
|
88
|
+
const { stdout, stderr } = await execFileAsync("git", args.filter(Boolean));
|
|
89
|
+
if (stderr && !stderr.includes("HEAD detached")) {
|
|
90
|
+
logger.warning(`Git branch command produced stderr`, { ...context, operation, stderr });
|
|
266
91
|
}
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
stderr: error.stderr,
|
|
275
|
-
stdout: error.stdout,
|
|
276
|
-
});
|
|
277
|
-
// Specific error handling
|
|
278
|
-
if (errorMessage.toLowerCase().includes("not a git repository")) {
|
|
279
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
|
|
280
|
-
}
|
|
281
|
-
if (input.mode === "create" && errorMessage.includes("already exists")) {
|
|
282
|
-
return {
|
|
283
|
-
success: false,
|
|
284
|
-
mode: "create",
|
|
285
|
-
message: `Failed to create branch: Branch '${input.branchName}' already exists. Use force=true to overwrite.`,
|
|
286
|
-
error: errorMessage,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
if (input.mode === "delete" && errorMessage.includes("not found")) {
|
|
290
|
-
return {
|
|
291
|
-
success: false,
|
|
292
|
-
mode: "delete",
|
|
293
|
-
message: `Failed to delete branch: Branch '${input.branchName}' not found.`,
|
|
294
|
-
error: errorMessage,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
if (input.mode === "delete" && errorMessage.includes("not fully merged")) {
|
|
298
|
-
return {
|
|
299
|
-
success: false,
|
|
300
|
-
mode: "delete",
|
|
301
|
-
message: `Failed to delete branch: Branch '${input.branchName}' is not fully merged. Use force=true to delete.`,
|
|
302
|
-
error: errorMessage,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
if (input.mode === "rename" && errorMessage.includes("already exists")) {
|
|
92
|
+
if (params.mode === "list") {
|
|
93
|
+
const branches = stdout.trim().split("\n").filter(Boolean).map(line => {
|
|
94
|
+
const isCurrent = line.startsWith("* ");
|
|
95
|
+
const trimmedLine = line.replace(/^\*?\s+/, "");
|
|
96
|
+
const isRemote = trimmedLine.startsWith("remotes/");
|
|
97
|
+
const parts = trimmedLine.split(/\s+/);
|
|
98
|
+
const name = parts[0];
|
|
306
99
|
return {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
100
|
+
name: isRemote ? name.split("/").slice(2).join("/") : name,
|
|
101
|
+
isCurrent,
|
|
102
|
+
isRemote,
|
|
103
|
+
commitHash: parts[1],
|
|
104
|
+
commitSubject: parts.slice(2).join(" "),
|
|
311
105
|
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
mode: params.mode,
|
|
110
|
+
message: `Found ${branches.length} branches.`,
|
|
111
|
+
branches,
|
|
112
|
+
currentBranch: branches.find(b => b.isCurrent)?.name || null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (params.mode === "show-current") {
|
|
116
|
+
const currentBranchName = stdout.trim() || null;
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
mode: params.mode,
|
|
120
|
+
message: currentBranchName ? `Current branch is '${currentBranchName}'.` : "Currently in detached HEAD state.",
|
|
121
|
+
currentBranch: currentBranchName,
|
|
122
|
+
};
|
|
323
123
|
}
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
mode: params.mode,
|
|
127
|
+
message: `Operation '${params.mode}' on branch '${params.branchName || params.newBranchName}' completed successfully.`,
|
|
128
|
+
};
|
|
324
129
|
}
|