Skip to content

Commit 4ff40ac

Browse files
renanliberatoclaude
andcommitted
refactor: align snippet file ref handling with repository/files pattern
getSnippetFileRawContent now takes an explicit ref string instead of a raw_url to parse. The ref-extraction logic is factored into a standalone extractSnippetRef helper, resolved once at the call site rather than once per file inside the fetch function. get_snippet also gains an optional ref parameter (matching get_file_contents) so callers can override the ref directly without relying on raw_url parsing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d378284 commit 4ff40ac

3 files changed

Lines changed: 86 additions & 16 deletions

File tree

index.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8380,18 +8380,11 @@ async function getSnippetRawContent(
83808380
}
83818381
83828382
/**
8383-
* Get the raw content of a specific file inside a multi-file snippet.
8384-
* Extracts the ref from the file's raw_url (e.g. .../raw/main/file.md) so we
8385-
* never have to guess it, then hits the proper REST API endpoint.
8383+
* Extract the ref (branch/tag/commit) from a snippet file's raw_url.
8384+
* Anchors on /snippets/{id}/raw/ so branch names or file paths containing
8385+
* the word "raw" don't produce a false match.
83868386
*/
8387-
async function getSnippetFileRawContent(
8388-
projectId: string | undefined,
8389-
snippetId: number,
8390-
rawUrl: string,
8391-
filePath: string
8392-
): Promise<string> {
8393-
// Anchor on the unambiguous /snippets/{id}/raw/ segment so branch names and
8394-
// file paths containing the word "raw" don't confuse the extraction.
8387+
function extractSnippetRef(rawUrl: string, snippetId: number, filePath: string): string {
83958388
const rawMarker = `/snippets/${snippetId}/raw/`;
83968389
const decoded = decodeURIComponent(new URL(rawUrl).pathname);
83978390
const markerIdx = decoded.indexOf(rawMarker);
@@ -8403,7 +8396,20 @@ async function getSnippetFileRawContent(
84038396
if (fileStart === -1) {
84048397
throw new Error(`Cannot locate file path "${filePath}" in snippet file raw_url: ${rawUrl}`);
84058398
}
8406-
const ref = afterRaw.slice(0, fileStart);
8399+
return afterRaw.slice(0, fileStart);
8400+
}
8401+
8402+
/**
8403+
* Fetch the raw content of one file inside a multi-file snippet.
8404+
* Accepts an explicit ref (branch/tag/commit) — callers resolve it via
8405+
* extractSnippetRef or a user-supplied parameter before calling this.
8406+
*/
8407+
async function getSnippetFileRawContent(
8408+
projectId: string | undefined,
8409+
snippetId: number,
8410+
ref: string,
8411+
filePath: string
8412+
): Promise<string> {
84078413
const encodedRef = encodeURIComponent(ref);
84088414
const encodedPath = filePath.split("/").map(encodeURIComponent).join("/");
84098415
const url = `${getSnippetsEndpoint(projectId)}/${snippetId}/files/${encodedRef}/${encodedPath}/raw`;
@@ -10570,11 +10576,14 @@ async function handleToolCall(params: any) {
1057010576
if (args.include_content) {
1057110577
const files = snippet.files ?? [];
1057210578
if (files.length > 1) {
10579+
const firstFile = files[0];
10580+
if (!firstFile.raw_url) throw new Error(`Snippet file "${firstFile.path}" has no raw_url`);
10581+
const ref = args.ref ?? extractSnippetRef(firstFile.raw_url, args.snippet_id, firstFile.path);
1057310582
result.files = await Promise.all(
10574-
files.map(async f => {
10575-
if (!f.raw_url) throw new Error(`Snippet file "${f.path}" has no raw_url`);
10576-
return { ...f, content: await getSnippetFileRawContent(args.project_id, args.snippet_id, f.raw_url, f.path) };
10577-
})
10583+
files.map(async f => ({
10584+
...f,
10585+
content: await getSnippetFileRawContent(args.project_id, args.snippet_id, ref, f.path),
10586+
}))
1057810587
);
1057910588
} else {
1058010589
result.content = await getSnippetRawContent(

schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3353,6 +3353,10 @@ export const GetSnippetSchema = z.object({
33533353
.optional()
33543354
.default(false)
33553355
.describe("Whether to fetch the raw file content (default: false)"),
3356+
ref: z
3357+
.string()
3358+
.optional()
3359+
.describe("Branch, tag, or commit to fetch content from. Inferred from the snippet's raw_url when omitted."),
33563360
});
33573361

33583362
export const SnippetFileSchema = z.object({

test/test-snippets.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const TEST_PERSONAL_SNIPPET_ID = 99;
1010
const TEST_MULTIFILE_SNIPPET_ID = 77;
1111
const TEST_MASTER_SNIPPET_ID = 88;
1212
const TEST_SLASH_REF_SNIPPET_ID = 101;
13+
const TEST_EXPLICIT_REF_SNIPPET_ID = 102;
1314
const RAW_CONTENT = "console.log('hello world');\n";
1415
const MULTIFILE_A_CONTENT = "# policy\nbody A\n";
1516
const MULTIFILE_B_CONTENT = "# instructions\nbody B\n";
@@ -296,6 +297,43 @@ describe("snippet tools", () => {
296297
(_req, res) => { res.type("text/plain").send(MULTIFILE_B_CONTENT); }
297298
);
298299

300+
// --- Explicit ref snippet: raw_url carries ref "wrong" but caller overrides with "v2" ---
301+
mockGitLab.addMockHandler(
302+
"get",
303+
`/projects/${TEST_PROJECT_ID}/snippets/${TEST_EXPLICIT_REF_SNIPPET_ID}`,
304+
(_req, res) => {
305+
res.json(
306+
buildSnippet({
307+
id: TEST_EXPLICIT_REF_SNIPPET_ID,
308+
title: "Explicit ref snippet",
309+
file_name: null,
310+
files: [
311+
{
312+
path: "a.md",
313+
raw_url: `${mockGitLabUrl}/-/snippets/${TEST_EXPLICIT_REF_SNIPPET_ID}/raw/wrong/a.md`,
314+
},
315+
{
316+
path: "b.md",
317+
raw_url: `${mockGitLabUrl}/-/snippets/${TEST_EXPLICIT_REF_SNIPPET_ID}/raw/wrong/b.md`,
318+
},
319+
],
320+
})
321+
);
322+
}
323+
);
324+
325+
mockGitLab.addMockHandler(
326+
"get",
327+
`/projects/${TEST_PROJECT_ID}/snippets/${TEST_EXPLICIT_REF_SNIPPET_ID}/files/v2/a.md/raw`,
328+
(_req, res) => { res.type("text/plain").send(MULTIFILE_A_CONTENT); }
329+
);
330+
331+
mockGitLab.addMockHandler(
332+
"get",
333+
`/projects/${TEST_PROJECT_ID}/snippets/${TEST_EXPLICIT_REF_SNIPPET_ID}/files/v2/b.md/raw`,
334+
(_req, res) => { res.type("text/plain").send(MULTIFILE_B_CONTENT); }
335+
);
336+
299337
// --- Personal snippet handlers ---
300338
mockGitLab.addMockHandler("get", "/snippets", (_req, res) => {
301339
res.json([
@@ -550,6 +588,25 @@ describe("snippet tools", () => {
550588
assert.strictEqual(result.files[1].content, MULTIFILE_B_CONTENT);
551589
});
552590

591+
test("get_snippet with include_content uses explicit ref when provided, ignoring raw_url ref", async () => {
592+
const result = await callTool(
593+
"get_snippet",
594+
{
595+
project_id: TEST_PROJECT_ID,
596+
snippet_id: TEST_EXPLICIT_REF_SNIPPET_ID,
597+
include_content: true,
598+
ref: "v2",
599+
},
600+
env()
601+
);
602+
603+
assert.strictEqual(result.id, TEST_EXPLICIT_REF_SNIPPET_ID);
604+
assert.ok(Array.isArray(result.files));
605+
assert.strictEqual(result.files.length, 2);
606+
assert.strictEqual(result.files[0].content, MULTIFILE_A_CONTENT);
607+
assert.strictEqual(result.files[1].content, MULTIFILE_B_CONTENT);
608+
});
609+
553610
test("create_snippet supports multi-file snippets via files[]", async () => {
554611
const result = await callTool(
555612
"create_snippet",

0 commit comments

Comments
 (0)