Skip to content

Commit 038edcc

Browse files
feat(web): add optional path filter to /api/diff and get_diff tool
Adds a `path` query/tool parameter to restrict diff output to changes touching a single file via git's `-- <pathspec>` separator. Refactors the route handler to use the shared `getDiffRequestSchema`. Fixes SOU-1154 (#1154) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5c36e73 commit 038edcc

6 files changed

Lines changed: 29 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Added commit history viewer to code browser. [#1150](https://github.com/sourcebot-dev/sourcebot/pull/1150)
1212
- Added `/api/commits/authors` to the public API to allow fetching a list of authors for a given path and revision. [#1150](https://github.com/sourcebot-dev/sourcebot/pull/1150)
13+
- Added optional `path` query parameter to the `/api/diff` endpoint and `get_diff` MCP tool to restrict diffs to changes touching a specific file. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154)
1314

1415
### Fixed
1516
- Bumped `postcss` to `8.5.10`. [#1155](https://github.com/sourcebot-dev/sourcebot/pull/1155)

docs/api-reference/sourcebot-public.openapi.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,16 @@
15851585
"description": "The head git ref (branch, tag, or commit SHA) to diff to.",
15861586
"name": "head",
15871587
"in": "query"
1588+
},
1589+
{
1590+
"schema": {
1591+
"type": "string",
1592+
"description": "Restrict the diff to changes touching this file path. Omit to diff all changes between the two refs."
1593+
},
1594+
"required": false,
1595+
"description": "Restrict the diff to changes touching this file path. Omit to diff all changes between the two refs.",
1596+
"name": "path",
1597+
"in": "query"
15881598
}
15891599
],
15901600
"responses": {

packages/web/src/app/api/(server)/diff/route.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
import { getDiff } from "@/features/git";
2+
import { getDiffRequestSchema } from "@/features/git/schemas";
23
import { apiHandler } from "@/lib/apiHandler";
34
import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
45
import { isServiceError } from "@/lib/utils";
56
import { NextRequest } from "next/server";
6-
import { z } from "zod";
7-
8-
const getDiffQueryParamsSchema = z.object({
9-
repo: z.string(),
10-
base: z.string(),
11-
head: z.string(),
12-
});
137

148
export const GET = apiHandler(async (request: NextRequest): Promise<Response> => {
159
const rawParams = Object.fromEntries(
16-
Object.keys(getDiffQueryParamsSchema.shape).map(key => [
10+
Object.keys(getDiffRequestSchema.shape).map(key => [
1711
key,
1812
request.nextUrl.searchParams.get(key) ?? undefined
1913
])
2014
);
21-
const parsed = getDiffQueryParamsSchema.safeParse(rawParams);
15+
const parsed = getDiffRequestSchema.safeParse(rawParams);
2216

2317
if (!parsed.success) {
2418
return serviceErrorResponse(

packages/web/src/features/git/getDiffApi.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ type GetDiffRequest = {
3232
repo: string;
3333
base: string;
3434
head: string;
35+
path?: string;
3536
}
3637

3738
export const getDiff = async ({
3839
repo: repoName,
3940
base,
4041
head,
42+
path,
4143
}: GetDiffRequest): Promise<GetDiffResult | ServiceError> => sew(() =>
4244
withOptionalAuth(async ({ org, prisma }) => {
4345
if (!isGitRefValid(base)) {
@@ -63,7 +65,15 @@ export const getDiff = async ({
6365
const git = simpleGit().cwd(repoPath);
6466

6567
try {
66-
const rawDiff = await git.raw(['diff', base, head]);
68+
const diffArgs: string[] = ['diff', base, head];
69+
// The `--` pathspec separator both restricts the diff to the path
70+
// and prevents anything path-shaped from being interpreted as a
71+
// flag or ref by git.
72+
if (path) {
73+
diffArgs.push('--', path);
74+
}
75+
76+
const rawDiff = await git.raw(diffArgs);
6777
const files = parseDiff(rawDiff);
6878

6979
const nodes: FileDiff[] = files.map((file) => ({

packages/web/src/features/git/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const getDiffRequestSchema = z.object({
4141
repo: z.string().describe('The fully-qualified repository name.'),
4242
base: z.string().describe('The base git ref (branch, tag, or commit SHA) to diff from.'),
4343
head: z.string().describe('The head git ref (branch, tag, or commit SHA) to diff to.'),
44+
path: z.string().optional().describe('Restrict the diff to changes touching this file path. Omit to diff all changes between the two refs.'),
4445
});
4546

4647
const hunkRangeSchema = z.object({

packages/web/src/features/tools/getDiff.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequest
2828
isIdempotent: true,
2929
description,
3030
inputSchema: getDiffRequestSchema,
31-
execute: async ({ repo, base, head }, _context) => {
32-
logger.debug('get_diff', { repo, base, head });
31+
execute: async ({ repo, base, head, path }, _context) => {
32+
logger.debug('get_diff', { repo, base, head, path });
3333

34-
const response = await getDiff({ repo, base, head });
34+
const response = await getDiff({ repo, base, head, path });
3535

3636
if (isServiceError(response)) {
3737
throw new Error(response.message);

0 commit comments

Comments
 (0)