Skip to content

Commit 12424fe

Browse files
authored
feat(mrs): add MR versions support (list and get diff versions) (#271)
* feat(mrs): add MR versions support (list and get diff versions) - Add `versions` action to list all diff versions of a merge request - Add `version` action to get specific version with file diffs - Add integration tests for new actions - Add documentation examples for versions/version actions Closes #268 * test(mrs): add unit tests for versions and version actions - Add unit tests for action: versions (list MR diff versions) - Add unit tests for action: version (get specific version) - Add superRefine validation test for version_id field - Update tool description assertions * fix(mrs): add passthrough to versions schemas for superRefine validation - Add .passthrough() to ListMergeRequestVersionsSchema - Add .passthrough() to GetMergeRequestVersionSchema - Add negative-case tests for action-field validation * test(mrs): use word boundary regex for version action check Use /\bversion\b/ instead of toContain("version") to ensure the test verifies distinct "version" action, not just a substring match from "versions". * fix(mrs): reject get/diffs-specific fields in versions/version actions Add superRefine validation to reject branch_name, include_diverged_commits_count, and include_rebase_in_progress fields when used with versions or version actions. These fields are only valid for get and diffs actions. * build(deps): add @types/picomatch for type safety Fixes TypeScript lint errors with picomatch usage in MR diffs handler. * refactor(mrs): rename constant to accurately describe purpose Rename getAndDiffsSharedFields to fieldsInvalidForVersionActions and add comment clarifying which fields are get-only vs shared. * fix(ci): generate prisma client before running tests coverage-pages.yml was missing prisma generate step, causing "Cannot find module prisma/client" errors. * test(mrs): use regex patterns for error message assertions Replace exact string matching with regex patterns to make tests more resilient to Zod error message format changes.
1 parent 2c2d31c commit 12424fe

File tree

8 files changed

+521
-5
lines changed

8 files changed

+521
-5
lines changed

.github/workflows/coverage-pages.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949
- name: Install dependencies
5050
run: yarn install --immutable
5151

52+
- name: Generate Prisma client
53+
run: yarn prisma generate
54+
5255
- name: Run tests with coverage
5356
run: yarn test:cov
5457

docs/tools/code-review.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ Find and inspect merge requests.
7777
}
7878
```
7979

80+
```json [List MR versions]
81+
{
82+
"action": "versions",
83+
"project_id": "my-org/api",
84+
"merge_request_iid": "42"
85+
}
86+
```
87+
88+
```json [Get specific version]
89+
{
90+
"action": "version",
91+
"project_id": "my-org/api",
92+
"merge_request_iid": "42",
93+
"version_id": "123"
94+
}
95+
```
96+
8097
:::
8198

8299
### Key Filters for `list`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@
473473
"@types/express": "^5.0.6",
474474
"@types/jest": "^30.0.0",
475475
"@types/node": "^24.10.9",
476-
"@types/picomatch": "^4.0.0",
476+
"@types/picomatch": "^4.0.2",
477477
"@typescript-eslint/eslint-plugin": "^8.54.0",
478478
"@typescript-eslint/parser": "^8.54.0",
479479
"auto-changelog": "^2.5.0",

src/entities/mrs/registry.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export const mrsToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinit
179179
{
180180
name: "browse_merge_requests",
181181
description:
182-
"Find and inspect merge requests. Actions: list (filter by state/author/reviewer/labels/branch), get (MR details by IID or source branch), diffs (file-level changes with inline suggestions), compare (diff between any two refs). Related: manage_merge_request to create/update/merge.",
182+
"Find and inspect merge requests. Actions: list (filter by state/author/reviewer/labels/branch), get (MR details by IID or source branch), diffs (file-level changes with inline suggestions), compare (diff between any two refs), versions (list diff versions from pushes), version (get specific version with diffs). Related: manage_merge_request to create/update/merge.",
183183
inputSchema: z.toJSONSchema(BrowseMergeRequestsSchema),
184184
gate: { envVar: "USE_MRS", defaultValue: true },
185185
handler: async (args: unknown) => {
@@ -309,6 +309,26 @@ export const mrsToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinit
309309
});
310310
}
311311

312+
case "versions": {
313+
// TypeScript knows: input has project_id (required), merge_request_iid (required)
314+
const { action: _action, project_id, merge_request_iid, ...rest } = input;
315+
const query = toQuery(rest, []);
316+
317+
return gitlab.get(
318+
`projects/${normalizeProjectId(project_id)}/merge_requests/${merge_request_iid}/versions`,
319+
{ query }
320+
);
321+
}
322+
323+
case "version": {
324+
// TypeScript knows: input has project_id (required), merge_request_iid (required), version_id (required)
325+
const { project_id, merge_request_iid, version_id } = input;
326+
327+
return gitlab.get(
328+
`projects/${normalizeProjectId(project_id)}/merge_requests/${merge_request_iid}/versions/${version_id}`
329+
);
330+
}
331+
312332
/* istanbul ignore next -- unreachable with Zod discriminatedUnion */
313333
default:
314334
throw new Error(`Unknown action: ${(input as { action: string }).action}`);

src/entities/mrs/schema-readonly.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { flexibleBoolean, requiredId, paginationFields } from "../utils";
33

44
// ============================================================================
55
// browse_merge_requests - CQRS Query Tool (discriminated union schema)
6-
// Actions: list, get, diffs, compare
6+
// Actions: list, get, diffs, compare, versions, version
77
// Uses z.discriminatedUnion() for type-safe action handling.
88
// Schema pipeline flattens to flat JSON Schema for AI clients that don't support oneOf.
99
// ============================================================================
@@ -202,6 +202,32 @@ const CompareMergeRequestSchema = z
202202
})
203203
.passthrough();
204204

205+
// --- Action: versions ---
206+
// Lists all diff versions of an MR. Each push creates a new version.
207+
// Note: .passthrough() preserves unknown fields for superRefine validation
208+
const ListMergeRequestVersionsSchema = z
209+
.object({
210+
action: z
211+
.literal("versions")
212+
.describe("List all diff versions of an MR (each push creates a version)"),
213+
project_id: projectIdField,
214+
merge_request_iid: mergeRequestIidField,
215+
...paginationFields(),
216+
})
217+
.passthrough();
218+
219+
// --- Action: version ---
220+
// Gets specific MR diff version with file changes
221+
// Note: .passthrough() preserves unknown fields for superRefine validation
222+
const GetMergeRequestVersionSchema = z
223+
.object({
224+
action: z.literal("version").describe("Get specific MR diff version with file changes"),
225+
project_id: projectIdField,
226+
merge_request_iid: mergeRequestIidField,
227+
version_id: requiredId.describe("Diff version ID from versions list"),
228+
})
229+
.passthrough();
230+
205231
// --- Discriminated union combining all actions ---
206232
// Note: GetMergeRequestSchema uses .refine() which doesn't work with discriminatedUnion directly,
207233
// so we use a two-step approach: discriminatedUnion for base validation, then refinement
@@ -210,6 +236,8 @@ const BrowseMergeRequestsBaseSchema = z.discriminatedUnion("action", [
210236
GetMergeRequestByIidSchema,
211237
DiffsMergeRequestSchema,
212238
CompareMergeRequestSchema,
239+
ListMergeRequestVersionsSchema,
240+
GetMergeRequestVersionSchema,
213241
]);
214242

215243
// Action-specific field sets for strict validation
@@ -250,7 +278,16 @@ const listOnlyFields = [
250278
];
251279
const compareOnlyFields = ["from", "to", "straight"];
252280
const getOnlyFields = ["merge_request_iid", "branch_name"];
281+
const versionOnlyFields = ["version_id"];
253282
const diffsOnlyFields = ["exclude_patterns", "exclude_lockfiles", "exclude_generated"];
283+
// Fields from get/diffs actions that are invalid for versions/version actions
284+
// - branch_name: get-only
285+
// - include_diverged_commits_count, include_rebase_in_progress: get and diffs
286+
const fieldsInvalidForVersionActions = [
287+
"branch_name",
288+
"include_diverged_commits_count",
289+
"include_rebase_in_progress",
290+
];
254291

255292
// Apply refinement for 'get' action validation and action-specific field validation
256293
export const BrowseMergeRequestsSchema = BrowseMergeRequestsBaseSchema.refine(
@@ -306,6 +343,19 @@ export const BrowseMergeRequestsSchema = BrowseMergeRequestsBaseSchema.refine(
306343
}
307344
}
308345

346+
// Check for version-only fields used in non-version actions
347+
if (data.action !== "version") {
348+
for (const field of versionOnlyFields) {
349+
if (field in input && input[field] !== undefined) {
350+
ctx.addIssue({
351+
code: z.ZodIssueCode.custom,
352+
message: `'${field}' is only valid for 'version' action`,
353+
path: [field],
354+
});
355+
}
356+
}
357+
}
358+
309359
// Check for diffs-only fields used in non-diffs actions
310360
if (data.action !== "diffs") {
311361
for (const field of diffsOnlyFields) {
@@ -318,6 +368,19 @@ export const BrowseMergeRequestsSchema = BrowseMergeRequestsBaseSchema.refine(
318368
}
319369
}
320370
}
371+
372+
// Check for get/diffs shared fields used in versions/version actions
373+
if (data.action === "versions" || data.action === "version") {
374+
for (const field of fieldsInvalidForVersionActions) {
375+
if (field in input && input[field] !== undefined) {
376+
ctx.addIssue({
377+
code: z.ZodIssueCode.custom,
378+
message: `'${field}' is not valid for '${data.action}' action`,
379+
path: [field],
380+
});
381+
}
382+
}
383+
}
321384
});
322385

323386
// ============================================================================

0 commit comments

Comments
 (0)