From 6c073fd5a09201faa3294613094e85f5ecb3ea7c Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 23:25:19 -0700 Subject: [PATCH 1/5] feat: add GET /api/commit endpoint and improve git API response formats Adds a new public API endpoint for retrieving details about a single commit including parent SHAs. Also improves existing git API responses by using camelCase field names and nullable types. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sourcebot-public.openapi.json | 136 +++++++++++++++++- docs/docs.json | 1 + packages/mcp/src/schemas.ts | 4 +- .../web/src/app/api/(server)/commit/route.ts | 30 ++++ packages/web/src/features/git/getCommitApi.ts | 103 +++++++++++++ packages/web/src/features/git/getDiffApi.ts | 8 +- packages/web/src/features/git/index.ts | 1 + .../src/features/git/listCommitsApi.test.ts | 16 +-- .../web/src/features/git/listCommitsApi.ts | 12 +- packages/web/src/features/git/schemas.ts | 17 ++- packages/web/src/openapi/publicApiDocument.ts | 23 +++ packages/web/src/openapi/publicApiSchemas.ts | 4 + 12 files changed, 330 insertions(+), 25 deletions(-) create mode 100644 packages/web/src/app/api/(server)/commit/route.ts create mode 100644 packages/web/src/features/git/getCommitApi.ts diff --git a/docs/api-reference/sourcebot-public.openapi.json b/docs/api-reference/sourcebot-public.openapi.json index e18303a8d..874d9197c 100644 --- a/docs/api-reference/sourcebot-public.openapi.json +++ b/docs/api-reference/sourcebot-public.openapi.json @@ -619,11 +619,13 @@ "properties": { "oldPath": { "type": "string", - "description": "The file path before the change. `/dev/null` for added files." + "nullable": true, + "description": "The file path before the change. `null` for added files." }, "newPath": { "type": "string", - "description": "The file path after the change. `/dev/null` for deleted files." + "nullable": true, + "description": "The file path after the change. `null` for deleted files." }, "hunks": { "type": "array", @@ -892,10 +894,10 @@ "type": "string", "description": "The commit body (everything after the subject line)." }, - "author_name": { + "authorName": { "type": "string" }, - "author_email": { + "authorEmail": { "type": "string" } }, @@ -905,8 +907,8 @@ "message", "refs", "body", - "author_name", - "author_email" + "authorName", + "authorEmail" ] }, "PublicListCommitsResponse": { @@ -915,6 +917,54 @@ "$ref": "#/components/schemas/PublicCommit" } }, + "PublicCommitDetail": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "description": "The full commit SHA." + }, + "date": { + "type": "string", + "description": "The commit date in ISO 8601 format." + }, + "message": { + "type": "string", + "description": "The commit subject line." + }, + "refs": { + "type": "string", + "description": "Refs pointing to this commit (e.g. branch or tag names)." + }, + "body": { + "type": "string", + "description": "The commit body (everything after the subject line)." + }, + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "parents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The parent commit SHAs." + } + }, + "required": [ + "hash", + "date", + "message", + "refs", + "body", + "authorName", + "authorEmail", + "parents" + ] + }, "PublicEeUser": { "type": "object", "properties": { @@ -1820,6 +1870,80 @@ } } }, + "/api/commit": { + "get": { + "operationId": "getCommit", + "tags": [ + "Git" + ], + "summary": "Get commit details", + "description": "Returns details for a single commit, including parent commit SHAs.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The fully-qualified repository name." + }, + "required": true, + "description": "The fully-qualified repository name.", + "name": "repo", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The git ref (commit SHA, branch, or tag)." + }, + "required": true, + "description": "The git ref (commit SHA, branch, or tag).", + "name": "ref", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Commit details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicCommitDetail" + } + } + } + }, + "400": { + "description": "Invalid query parameters or git ref.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiServiceError" + } + } + } + }, + "404": { + "description": "Repository or revision not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiServiceError" + } + } + } + }, + "500": { + "description": "Unexpected failure.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiServiceError" + } + } + } + } + } + } + }, "/api/ee/user": { "get": { "operationId": "getUser", diff --git a/docs/docs.json b/docs/docs.json index 2899fae8a..50e9e7990 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -172,6 +172,7 @@ "group": "Git", "icon": "code-branch", "pages": [ + "GET /api/commit", "GET /api/diff", "GET /api/commits", "GET /api/source", diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts index a72fbf116..9be5a454f 100644 --- a/packages/mcp/src/schemas.ts +++ b/packages/mcp/src/schemas.ts @@ -360,8 +360,8 @@ export const listCommitsResponseSchema = z.array(z.object({ message: z.string(), refs: z.string(), body: z.string(), - author_name: z.string(), - author_email: z.string(), + authorName: z.string(), + authorEmail: z.string(), })); export const languageModelInfoSchema = z.object({ diff --git a/packages/web/src/app/api/(server)/commit/route.ts b/packages/web/src/app/api/(server)/commit/route.ts new file mode 100644 index 000000000..c5e409ce8 --- /dev/null +++ b/packages/web/src/app/api/(server)/commit/route.ts @@ -0,0 +1,30 @@ +import { getCommit } from "@/features/git"; +import { getCommitQueryParamsSchema } from "@/features/git/schemas"; +import { apiHandler } from "@/lib/apiHandler"; +import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; +import { isServiceError } from "@/lib/utils"; +import { NextRequest } from "next/server"; + +export const GET = apiHandler(async (request: NextRequest): Promise => { + const rawParams = Object.fromEntries( + Object.keys(getCommitQueryParamsSchema.shape).map(key => [ + key, + request.nextUrl.searchParams.get(key) ?? undefined + ]) + ); + const parsed = getCommitQueryParamsSchema.safeParse(rawParams); + + if (!parsed.success) { + return serviceErrorResponse( + queryParamsSchemaValidationError(parsed.error) + ); + } + + const result = await getCommit(parsed.data); + + if (isServiceError(result)) { + return serviceErrorResponse(result); + } + + return Response.json(result); +}); diff --git a/packages/web/src/features/git/getCommitApi.ts b/packages/web/src/features/git/getCommitApi.ts new file mode 100644 index 000000000..5e4843629 --- /dev/null +++ b/packages/web/src/features/git/getCommitApi.ts @@ -0,0 +1,103 @@ +import { sew } from "@/middleware/sew"; +import { invalidGitRef, notFound, ServiceError, unexpectedError } from '@/lib/serviceError'; +import { withOptionalAuth } from '@/middleware/withAuth'; +import { getRepoPath } from '@sourcebot/shared'; +import { z } from 'zod'; +import { simpleGit } from 'simple-git'; +import { commitDetailSchema } from './schemas'; +import { isGitRefValid } from './utils'; + +export type CommitDetail = z.infer; + +type GetCommitRequest = { + repo: string; + ref: string; +} + +// Field separator that won't appear in commit data +const FIELD_SEP = '\x1f'; +const FORMAT = [ + '%H', // hash + '%aI', // author date ISO 8601 + '%s', // subject + '%D', // refs + '%b', // body + '%aN', // author name + '%aE', // author email + '%P', // parent hashes (space-separated) +].join(FIELD_SEP); + +export const getCommit = async ({ + repo: repoName, + ref, +}: GetCommitRequest): Promise => sew(() => + withOptionalAuth(async ({ org, prisma }) => { + const repo = await prisma.repo.findFirst({ + where: { + name: repoName, + orgId: org.id, + }, + }); + + if (!repo) { + return notFound(`Repository "${repoName}" not found.`); + } + + if (!isGitRefValid(ref)) { + return invalidGitRef(ref); + } + + const { path: repoPath } = getRepoPath(repo); + const git = simpleGit().cwd(repoPath); + + try { + const output = (await git.raw([ + 'log', + '-1', + `--format=${FORMAT}`, + ref, + ])).trim(); + + const fields = output.split(FIELD_SEP); + if (fields.length < 8) { + return unexpectedError(`Failed to parse commit data for revision "${ref}".`); + } + + const [hash, date, message, refs, body, authorName, authorEmail, parentStr] = fields; + const parents = parentStr.trim() === '' ? [] : parentStr.trim().split(' '); + + return { + hash, + date, + message, + refs, + body, + authorName, + authorEmail, + parents, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + + if (errorMessage.includes('not a git repository')) { + return unexpectedError( + `Invalid git repository at ${repoPath}. ` + + `The directory exists but is not a valid git repository.` + ); + } + + if (errorMessage.includes('unknown revision') || errorMessage.includes('bad object')) { + return notFound(`Revision "${ref}" not found in repository "${repoName}".`); + } + + if (error instanceof Error) { + throw new Error( + `Failed to get commit in repository ${repoName}: ${error.message}` + ); + } else { + throw new Error( + `Failed to get commit in repository ${repoName}: ${errorMessage}` + ); + } + } + })); diff --git a/packages/web/src/features/git/getDiffApi.ts b/packages/web/src/features/git/getDiffApi.ts index 9913b497a..6218280d7 100644 --- a/packages/web/src/features/git/getDiffApi.ts +++ b/packages/web/src/features/git/getDiffApi.ts @@ -19,8 +19,8 @@ export interface DiffHunk { } export interface FileDiff { - oldPath: string; - newPath: string; + oldPath: string | null; + newPath: string | null; hunks: DiffHunk[]; } @@ -67,8 +67,8 @@ export const getDiff = async ({ const files = parseDiff(rawDiff); const nodes: FileDiff[] = files.map((file) => ({ - oldPath: file.from ?? '/dev/null', - newPath: file.to ?? '/dev/null', + oldPath: file.from && file.from !== '/dev/null' ? file.from : null, + newPath: file.to && file.to !== '/dev/null' ? file.to : null, hunks: file.chunks.map((chunk) => { // chunk.content is the full @@ header line, e.g.: // "@@ -7,6 +7,8 @@ some heading text" diff --git a/packages/web/src/features/git/index.ts b/packages/web/src/features/git/index.ts index 4adb81966..579d81017 100644 --- a/packages/web/src/features/git/index.ts +++ b/packages/web/src/features/git/index.ts @@ -1,3 +1,4 @@ +export * from './getCommitApi'; export * from './getDiffApi'; export * from './getFilesApi'; export * from './getFolderContentsApi'; diff --git a/packages/web/src/features/git/listCommitsApi.test.ts b/packages/web/src/features/git/listCommitsApi.test.ts index af02bd2cc..1d77a8a17 100644 --- a/packages/web/src/features/git/listCommitsApi.test.ts +++ b/packages/web/src/features/git/listCommitsApi.test.ts @@ -321,8 +321,8 @@ describe('searchCommits', () => { message: 'feat: add feature', refs: 'HEAD -> main', body: '', - author_name: 'John Doe', - author_email: 'john@example.com', + authorName: 'John Doe', + authorEmail: 'john@example.com', }, { hash: 'def456', @@ -330,8 +330,8 @@ describe('searchCommits', () => { message: 'fix: bug fix', refs: '', body: '', - author_name: 'Jane Smith', - author_email: 'jane@example.com', + authorName: 'Jane Smith', + authorEmail: 'jane@example.com', }, ]; @@ -458,8 +458,8 @@ describe('searchCommits', () => { message: 'fix: resolve authentication bug', refs: 'HEAD -> main', body: 'Fixed issue with JWT token validation', - author_name: 'Security Team', - author_email: 'security@example.com', + authorName: 'Security Team', + authorEmail: 'security@example.com', }, ]; @@ -562,8 +562,8 @@ describe('searchCommits', () => { message: 'feat: new feature', refs: 'main', body: 'Added new functionality', - author_name: 'Developer', - author_email: 'dev@example.com', + authorName: 'Developer', + authorEmail: 'dev@example.com', }, ]; diff --git a/packages/web/src/features/git/listCommitsApi.ts b/packages/web/src/features/git/listCommitsApi.ts index 0e8bf113c..715aeae83 100644 --- a/packages/web/src/features/git/listCommitsApi.ts +++ b/packages/web/src/features/git/listCommitsApi.ts @@ -117,7 +117,17 @@ export const listCommits = async ({ const totalCount = parseInt((await git.raw(countArgs)).trim(), 10); - return { commits: log.all as unknown as Commit[], totalCount }; + const commits: Commit[] = log.all.map((c) => ({ + hash: c.hash, + date: c.date, + message: c.message, + refs: c.refs, + body: c.body, + authorName: c.author_name, + authorEmail: c.author_email, + })); + + return { commits, totalCount }; } catch (error: unknown) { // Provide detailed error messages for common git errors const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/web/src/features/git/schemas.ts b/packages/web/src/features/git/schemas.ts index 4a41bba38..5247527ca 100644 --- a/packages/web/src/features/git/schemas.ts +++ b/packages/web/src/features/git/schemas.ts @@ -56,8 +56,8 @@ const diffHunkSchema = z.object({ }); const fileDiffSchema = z.object({ - oldPath: z.string().describe('The file path before the change. `/dev/null` for added files.'), - newPath: z.string().describe('The file path after the change. `/dev/null` for deleted files.'), + oldPath: z.string().nullable().describe('The file path before the change. `null` for added files.'), + newPath: z.string().nullable().describe('The file path after the change. `null` for deleted files.'), hunks: z.array(diffHunkSchema).describe('The list of changed regions within the file.'), }); @@ -83,6 +83,15 @@ export const commitSchema = z.object({ message: z.string().describe('The commit subject line.'), refs: z.string().describe('Refs pointing to this commit (e.g. branch or tag names).'), body: z.string().describe('The commit body (everything after the subject line).'), - author_name: z.string(), - author_email: z.string(), + authorName: z.string(), + authorEmail: z.string(), +}); + +export const getCommitQueryParamsSchema = z.object({ + repo: z.string().describe('The fully-qualified repository name.'), + ref: z.string().describe('The git ref (commit SHA, branch, or tag).'), +}); + +export const commitDetailSchema = commitSchema.extend({ + parents: z.array(z.string()).describe('The parent commit SHAs.'), }); diff --git a/packages/web/src/openapi/publicApiDocument.ts b/packages/web/src/openapi/publicApiDocument.ts index b6e9c23bf..147c43c88 100644 --- a/packages/web/src/openapi/publicApiDocument.ts +++ b/packages/web/src/openapi/publicApiDocument.ts @@ -16,6 +16,8 @@ import { publicGetDiffResponseSchema, publicGetTreeRequestSchema, publicHealthResponseSchema, + publicCommitDetailSchema, + publicGetCommitQuerySchema, publicListCommitsQuerySchema, publicListCommitsResponseSchema, publicListReposQueryParamsSchema, @@ -339,6 +341,27 @@ export function createPublicOpenApiDocument(version: string) { }, }); + registry.registerPath({ + method: 'get', + path: '/api/commit', + operationId: 'getCommit', + tags: [gitTag.name], + summary: 'Get commit details', + description: 'Returns details for a single commit, including parent commit SHAs.', + request: { + query: publicGetCommitQuerySchema, + }, + responses: { + 200: { + description: 'Commit details.', + content: jsonContent(publicCommitDetailSchema), + }, + 400: errorJson('Invalid query parameters or git ref.'), + 404: errorJson('Repository or revision not found.'), + 500: errorJson('Unexpected failure.'), + }, + }); + // EE: User Management registry.registerPath({ method: 'get', diff --git a/packages/web/src/openapi/publicApiSchemas.ts b/packages/web/src/openapi/publicApiSchemas.ts index f0d8b06f6..dae94afc1 100644 --- a/packages/web/src/openapi/publicApiSchemas.ts +++ b/packages/web/src/openapi/publicApiSchemas.ts @@ -5,9 +5,11 @@ import { findRelatedSymbolsResponseSchema, } from '../features/codeNav/types.js'; import { + commitDetailSchema, commitSchema, fileSourceRequestSchema, fileSourceResponseSchema, + getCommitQueryParamsSchema, getDiffRequestSchema, getDiffResponseSchema, getTreeRequestSchema, @@ -46,6 +48,8 @@ export const publicFindSymbolsResponseSchema = findRelatedSymbolsResponseSchema. export const publicListCommitsQuerySchema = listCommitsQueryParamsSchema.openapi('PublicListCommitsQuery'); export const publicCommitSchema = commitSchema.openapi('PublicCommit'); export const publicListCommitsResponseSchema = z.array(publicCommitSchema).openapi('PublicListCommitsResponse'); +export const publicGetCommitQuerySchema = getCommitQueryParamsSchema.openapi('PublicGetCommitQuery'); +export const publicCommitDetailSchema = commitDetailSchema.openapi('PublicCommitDetail'); export const publicHealthResponseSchema = z.object({ status: z.enum(['ok']), From bd134d9e0f5dae3ff0c4727fead124b6edcfe59c Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 23:25:44 -0700 Subject: [PATCH 2/5] docs: add CHANGELOG entries for #1077 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e65168e0d..1c01e85dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added `GET /api/commit` endpoint for retrieving details about a single commit, including parent commit SHAs [#1077](https://github.com/sourcebot-dev/sourcebot/pull/1077) + ### Changed - Replaced placeholder avatars with deterministic minidenticon-based avatars generated from email addresses [#1072](https://github.com/sourcebot-dev/sourcebot/pull/1072) +- Changed `author_name` and `author_email` fields to `authorName` and `authorEmail` in `GET /api/commits` response [#1077](https://github.com/sourcebot-dev/sourcebot/pull/1077) +- Changed `oldPath` and `newPath` in `GET /api/diff` response from `"/dev/null"` to `null` for added/deleted files [#1077](https://github.com/sourcebot-dev/sourcebot/pull/1077) ## [4.16.4] - 2026-04-01 From 09002ca0501f21fb7eca52520b86341c6be1a56e Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 23:31:14 -0700 Subject: [PATCH 3/5] nits --- packages/web/src/features/git/listCommitsApi.ts | 6 +++--- packages/web/src/features/tools/listCommits.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/web/src/features/git/listCommitsApi.ts b/packages/web/src/features/git/listCommitsApi.ts index 715aeae83..362f8c78b 100644 --- a/packages/web/src/features/git/listCommitsApi.ts +++ b/packages/web/src/features/git/listCommitsApi.ts @@ -10,10 +10,10 @@ import { isGitRefValid } from './utils'; export type Commit = z.infer; -export interface SearchCommitsResult { +export type ListCommitsResponse = { commits: Commit[]; totalCount: number; -} +}; type ListCommitsRequest = { repo: string; @@ -44,7 +44,7 @@ export const listCommits = async ({ path, maxCount = 50, skip = 0, -}: ListCommitsRequest): Promise => sew(() => +}: ListCommitsRequest): Promise => sew(() => withOptionalAuth(async ({ org, prisma }) => { const repo = await prisma.repo.findFirst({ where: { diff --git a/packages/web/src/features/tools/listCommits.ts b/packages/web/src/features/tools/listCommits.ts index 34d61eeeb..abdb1f59c 100644 --- a/packages/web/src/features/tools/listCommits.ts +++ b/packages/web/src/features/tools/listCommits.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { isServiceError } from "@/lib/utils"; -import { listCommits, SearchCommitsResult } from "@/features/git"; +import { listCommits, ListCommitsResponse } from "@/features/git"; import { ToolDefinition } from "./types"; import { logger } from "./logger"; import description from "./listCommits.txt"; @@ -25,7 +25,7 @@ export type ListCommitsRepoInfo = { codeHostType: CodeHostType; }; -export type ListCommitsMetadata = SearchCommitsResult & { +export type ListCommitsMetadata = ListCommitsResponse & { repo: string; repoInfo: ListCommitsRepoInfo; }; From ded70c8c4bcb044e47dc6ecfc4d14991c161ca42 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 23:31:59 -0700 Subject: [PATCH 4/5] fix: update tests to mock simple-git snake_case output separately from camelCase expected results Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/git/listCommitsApi.test.ts | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/web/src/features/git/listCommitsApi.test.ts b/packages/web/src/features/git/listCommitsApi.test.ts index 1d77a8a17..1c4249f87 100644 --- a/packages/web/src/features/git/listCommitsApi.test.ts +++ b/packages/web/src/features/git/listCommitsApi.test.ts @@ -314,7 +314,28 @@ describe('searchCommits', () => { describe('successful responses', () => { it('should return commits and totalCount from git log', async () => { - const mockCommits = [ + const gitLogOutput = [ + { + hash: 'abc123', + date: '2024-06-15', + message: 'feat: add feature', + refs: 'HEAD -> main', + body: '', + author_name: 'John Doe', + author_email: 'john@example.com', + }, + { + hash: 'def456', + date: '2024-06-14', + message: 'fix: bug fix', + refs: '', + body: '', + author_name: 'Jane Smith', + author_email: 'jane@example.com', + }, + ]; + + const expectedCommits = [ { hash: 'abc123', date: '2024-06-15', @@ -335,14 +356,14 @@ describe('searchCommits', () => { }, ]; - mockGitLog.mockResolvedValue({ all: mockCommits }); + mockGitLog.mockResolvedValue({ all: gitLogOutput }); mockGitRaw.mockResolvedValue('2'); const result = await listCommits({ repo: 'github.com/test/repo', }); - expect(result).toEqual({ commits: mockCommits, totalCount: 2 }); + expect(result).toEqual({ commits: expectedCommits, totalCount: 2 }); }); it('should return empty commits array when no commits match', async () => { @@ -451,7 +472,19 @@ describe('searchCommits', () => { describe('integration scenarios', () => { it('should handle a typical commit search with filters', async () => { - const mockCommits = [ + const gitLogOutput = [ + { + hash: 'abc123', + date: '2024-06-10T14:30:00Z', + message: 'fix: resolve authentication bug', + refs: 'HEAD -> main', + body: 'Fixed issue with JWT token validation', + author_name: 'Security Team', + author_email: 'security@example.com', + }, + ]; + + const expectedCommits = [ { hash: 'abc123', date: '2024-06-10T14:30:00Z', @@ -465,7 +498,7 @@ describe('searchCommits', () => { vi.spyOn(dateUtils, 'validateDateRange').mockReturnValue(null); vi.spyOn(dateUtils, 'toGitDate').mockImplementation((date) => date); - mockGitLog.mockResolvedValue({ all: mockCommits }); + mockGitLog.mockResolvedValue({ all: gitLogOutput }); mockGitRaw.mockResolvedValue('1'); const result = await listCommits({ @@ -477,7 +510,7 @@ describe('searchCommits', () => { maxCount: 20, }); - expect(result).toEqual({ commits: mockCommits, totalCount: 1 }); + expect(result).toEqual({ commits: expectedCommits, totalCount: 1 }); expect(mockGitLog).toHaveBeenCalledWith([ '--max-count=20', '--since=30 days ago', @@ -555,7 +588,19 @@ describe('searchCommits', () => { }); it('should work end-to-end with repository lookup', async () => { - const mockCommits = [ + const gitLogOutput = [ + { + hash: 'xyz789', + date: '2024-06-20T10:00:00Z', + message: 'feat: new feature', + refs: 'main', + body: 'Added new functionality', + author_name: 'Developer', + author_email: 'dev@example.com', + }, + ]; + + const expectedCommits = [ { hash: 'xyz789', date: '2024-06-20T10:00:00Z', @@ -570,7 +615,7 @@ describe('searchCommits', () => { mockFindFirst.mockResolvedValue({ id: 555, name: 'github.com/test/repository' }); vi.spyOn(dateUtils, 'validateDateRange').mockReturnValue(null); vi.spyOn(dateUtils, 'toGitDate').mockImplementation((date) => date); - mockGitLog.mockResolvedValue({ all: mockCommits }); + mockGitLog.mockResolvedValue({ all: gitLogOutput }); mockGitRaw.mockResolvedValue('1'); const result = await listCommits({ @@ -580,7 +625,7 @@ describe('searchCommits', () => { author: 'Developer', }); - expect(result).toEqual({ commits: mockCommits, totalCount: 1 }); + expect(result).toEqual({ commits: expectedCommits, totalCount: 1 }); expect(mockCwd).toHaveBeenCalledWith('/mock/cache/dir/555'); }); }); From 92363c69979b3d32f9664a15331efb4c87090e4d Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 23:37:43 -0700 Subject: [PATCH 5/5] revert changes to deprecated mcp package --- packages/mcp/src/schemas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts index 9be5a454f..a72fbf116 100644 --- a/packages/mcp/src/schemas.ts +++ b/packages/mcp/src/schemas.ts @@ -360,8 +360,8 @@ export const listCommitsResponseSchema = z.array(z.object({ message: z.string(), refs: z.string(), body: z.string(), - authorName: z.string(), - authorEmail: z.string(), + author_name: z.string(), + author_email: z.string(), })); export const languageModelInfoSchema = z.object({