From cdf4697ef1c66e4d614de10585bd595061cbc740 Mon Sep 17 00:00:00 2001 From: iwakitakuma33 Date: Wed, 11 Jun 2025 03:51:32 +0900 Subject: [PATCH] FEAT: get list commits issue: #92 --- index.ts | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++ schemas.ts | 40 ++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/index.ts b/index.ts index 953666c..6417f80 100644 --- a/index.ts +++ b/index.ts @@ -172,6 +172,12 @@ import { GitLabCompareResultSchema, GetBranchDiffsSchema, ListWikiPagesOptions, + ListCommitsSchema, + GetCommitSchema, + GetCommitDiffSchema, + type ListCommitsOptions, + type GetCommitOptions, + type GetCommitDiffOptions, } from "./schemas.js"; /** @@ -596,6 +602,21 @@ const allTools = [ description: "Get GitLab user details by usernames", inputSchema: zodToJsonSchema(GetUsersSchema), }, + { + name: "list_commits", + description: "List repository commits with filtering options", + inputSchema: zodToJsonSchema(ListCommitsSchema), + }, + { + name: "get_commit", + description: "Get details of a specific commit", + inputSchema: zodToJsonSchema(GetCommitSchema), + }, + { + name: "get_commit_diff", + description: "Get changes/diffs of a specific commit", + inputSchema: zodToJsonSchema(GetCommitDiffSchema), + }, ]; // Define which tools are read-only @@ -634,6 +655,9 @@ const readOnlyTools = [ "list_wiki_pages", "get_wiki_page", "get_users", + "list_commits", + "get_commit", + "get_commit_diff", ]; // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI @@ -3039,6 +3063,107 @@ async function getUsers(usernames: string[]): Promise { return GitLabUsersResponseSchema.parse(users); } +/** + * List repository commits + * 저장소 커밋 목록 조회 + * + * @param {string} projectId - Project ID or URL-encoded path + * @param {ListCommitsOptions} options - List commits options + * @returns {Promise} List of commits + */ +async function listCommits( + projectId: string, + options: Omit = {} +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits` + ); + + // Add query parameters + if (options.ref_name) url.searchParams.append("ref_name", options.ref_name); + if (options.since) url.searchParams.append("since", options.since); + if (options.until) url.searchParams.append("until", options.until); + if (options.path) url.searchParams.append("path", options.path); + if (options.author) url.searchParams.append("author", options.author); + if (options.all) url.searchParams.append("all", options.all.toString()); + if (options.with_stats) url.searchParams.append("with_stats", options.with_stats.toString()); + if (options.first_parent) url.searchParams.append("first_parent", options.first_parent.toString()); + if (options.order) url.searchParams.append("order", options.order); + if (options.trailers) url.searchParams.append("trailers", options.trailers.toString()); + if (options.page) url.searchParams.append("page", options.page.toString()); + if (options.per_page) url.searchParams.append("per_page", options.per_page.toString()); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + + const data = await response.json(); + return z.array(GitLabCommitSchema).parse(data); +} + +/** + * Get a single commit + * 단일 커밋 정보 조회 + * + * @param {string} projectId - Project ID or URL-encoded path + * @param {string} sha - The commit hash or name of a repository branch or tag + * @param {boolean} [stats] - Include commit stats + * @returns {Promise} The commit details + */ +async function getCommit( + projectId: string, + sha: string, + stats?: boolean +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits/${encodeURIComponent(sha)}` + ); + + if (stats) { + url.searchParams.append("stats", "true"); + } + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + + const data = await response.json(); + return GitLabCommitSchema.parse(data); +} + +/** + * Get commit diff + * 커밋 변경사항 조회 + * + * @param {string} projectId - Project ID or URL-encoded path + * @param {string} sha - The commit hash or name of a repository branch or tag + * @returns {Promise} The commit diffs + */ +async function getCommitDiff( + projectId: string, + sha: string +): Promise { + projectId = decodeURIComponent(projectId); + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits/${encodeURIComponent(sha)}/diff` + ); + + const response = await fetch(url.toString(), { + ...DEFAULT_FETCH_CONFIG, + }); + + await handleGitLabError(response); + + const data = await response.json(); + return z.array(GitLabDiffSchema).parse(data); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { // Apply read-only filter first const tools0 = GITLAB_READ_ONLY_MODE @@ -3933,6 +4058,30 @@ server.setRequestHandler(CallToolRequestSchema, async request => { }; } + case "list_commits": { + const args = ListCommitsSchema.parse(request.params.arguments); + const commits = await listCommits(args.project_id, args); + return { + content: [{ type: "text", text: JSON.stringify(commits, null, 2) }], + }; + } + + case "get_commit": { + const args = GetCommitSchema.parse(request.params.arguments); + const commit = await getCommit(args.project_id, args.sha, args.stats); + return { + content: [{ type: "text", text: JSON.stringify(commit, null, 2) }], + }; + } + + case "get_commit_diff": { + const args = GetCommitDiffSchema.parse(request.params.arguments); + const diff = await getCommitDiff(args.project_id, args.sha); + return { + content: [{ type: "text", text: JSON.stringify(diff, null, 2) }], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/schemas.ts b/schemas.ts index fd5a132..50e548e 100644 --- a/schemas.ts +++ b/schemas.ts @@ -407,8 +407,17 @@ export const GitLabCommitSchema = z.object({ committer_name: z.string(), committer_email: z.string(), committed_date: z.string(), + created_at: z.string().optional(), // Add created_at field + message: z.string().optional(), // Add full message field web_url: z.string(), // Changed from html_url to match GitLab API parent_ids: z.array(z.string()), // Changed from parents to match GitLab API + stats: z.object({ + additions: z.number().optional().nullable(), + deletions: z.number().optional().nullable(), + total: z.number().optional().nullable(), + }).optional(), // Only present when with_stats=true + trailers: z.record(z.string()).optional().default({}), // Git trailers, may be empty object + extended_trailers: z.record(z.array(z.string())).optional().default({}), // Extended trailers, may be empty object }); // Reference schema @@ -1328,6 +1337,34 @@ export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema; // Schema for getting burndown chart events for a milestone export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema); +// Add schemas for commit operations +export const ListCommitsSchema = z.object({ + project_id: z.string().describe("Project ID or complete URL-encoded path to project"), + ref_name: z.string().optional().describe("The name of a repository branch, tag or revision range, or if not given the default branch"), + since: z.string().optional().describe("Only commits after or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"), + until: z.string().optional().describe("Only commits before or on this date are returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ"), + path: z.string().optional().describe("The file path"), + author: z.string().optional().describe("Search commits by commit author"), + all: z.boolean().optional().describe("Retrieve every commit from the repository"), + with_stats: z.boolean().optional().describe("Stats about each commit are added to the response"), + first_parent: z.boolean().optional().describe("Follow only the first parent commit upon seeing a merge commit"), + order: z.enum(["default", "topo"]).optional().describe("List commits in order"), + trailers: z.boolean().optional().describe("Parse and include Git trailers for every commit"), + page: z.number().optional().describe("Page number for pagination (default: 1)"), + per_page: z.number().optional().describe("Number of items per page (max: 100, default: 20)"), +}); + +export const GetCommitSchema = z.object({ + project_id: z.string().describe("Project ID or complete URL-encoded path to project"), + sha: z.string().describe("The commit hash or name of a repository branch or tag"), + stats: z.boolean().optional().describe("Include commit stats"), +}); + +export const GetCommitDiffSchema = z.object({ + project_id: z.string().describe("Project ID or complete URL-encoded path to project"), + sha: z.string().describe("The commit hash or name of a repository branch or tag"), +}); + // Export types export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -1394,3 +1431,6 @@ export type GetMilestoneBurndownEventsOptions = z.infer; export type GitLabUsersResponse = z.infer; export type PaginationOptions = z.infer; +export type ListCommitsOptions = z.infer; +export type GetCommitOptions = z.infer; +export type GetCommitDiffOptions = z.infer;