Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1574,17 +1574,17 @@ async function getDefaultBranchRef(projectId: string): Promise<string> {
* @returns {Promise<GitLabContent>} The file content
*/
async function getFileContents(
projectId: string,
projectId: string | undefined,
filePath: string,
ref?: string
): Promise<GitLabContent> {
projectId = decodeURIComponent(projectId); // Decode project ID
const effectiveProjectId = getEffectiveProjectId(projectId);
const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
const encodedPath = encodeURIComponent(filePath);

// ref가 없는 경우 default branch를 가져옴
if (!ref) {
ref = await getDefaultBranchRef(projectId);
ref = await getDefaultBranchRef(decodedProjectId);
}

const url = new URL(
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"test:mock": "npx tsx --test test/remote-auth-simple-test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && tsx test/test-list-project-members.ts",
"test:live": "node test/validate-api.js",
"test:remote-auth": "npm run build && npx tsx --test test/remote-auth-simple-test.ts",
"test:schema": "tsx test/schema-tests.ts",
"test:oauth": "tsx test/oauth-tests.ts",
"test:list-merge-requests": "npm run build && tsx test/test-list-merge-requests.ts",
"test:approvals": "npm run build && tsx test/test-merge-request-approvals.ts",
Expand Down
61 changes: 48 additions & 13 deletions schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,17 +475,17 @@ export const GitLabProjectSchema = GitLabRepositorySchema;

// File content schemas
export const GitLabFileContentSchema = z.object({
file_name: z.string(), // Changed from name to match GitLab API
file_path: z.string(), // Changed from path to match GitLab API
size: z.number(),
file_name: z.string().optional(),
file_path: z.string(),
size: z.coerce.number().optional(),
encoding: z.string(),
content: z.string(),
content_sha256: z.string(), // Changed from sha to match GitLab API
ref: z.string(), // Added as GitLab requires branch reference
blob_id: z.string(), // Added to match GitLab API
commit_id: z.string(), // ID of the current file version
last_commit_id: z.string(), // Added to match GitLab API
execute_filemode: z.boolean().optional(), // Added to match GitLab API
content_sha256: z.string().optional(),
ref: z.string().optional(),
blob_id: z.string().optional(),
commit_id: z.string().optional(),
last_commit_id: z.string().optional(),
execute_filemode: z.boolean().optional(),
});

export const GitLabDirectoryContentSchema = z.object({
Expand Down Expand Up @@ -1058,10 +1058,45 @@ export const CreateRepositorySchema = z.object({
initialize_with_readme: z.boolean().optional().describe("Initialize with README.md"),
});

export const GetFileContentsSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path to the file or directory"),
ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
});
export const GetFileContentsSchema = z
.object({
project_id: z.coerce
.string()
.optional()
.describe("Project ID or URL-encoded path (optional; falls back to env)"),
file_path: z
.string()
.optional()
.describe(
"Path to the file or directory. Takes precedence over 'path' when both are provided"
),
path: z.string().optional().describe("Alias of file_path"),
ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
})
.superRefine((data, ctx) => {
const fp = data.file_path?.trim();
const p = data.path?.trim();
if (!fp && !p) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Either 'file_path' or 'path' must be provided",
path: ["file_path"],
});
}
const finalPath = fp && fp.length > 0 ? fp : p ?? "";
if (finalPath.trim().length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "file_path cannot be empty or whitespace",
path: ["file_path"],
});
}
})
.transform(data => ({
project_id: (data.project_id ?? "").trim() || undefined,
file_path: ((data.file_path ?? "").trim() || (data.path ?? "").trim()).trim(),
ref: (data.ref ?? "").trim() || undefined,
}));

export const PushFilesSchema = ProjectParamsSchema.extend({
branch: z.string().describe("Branch to push to"),
Expand Down
101 changes: 101 additions & 0 deletions test/schema-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env ts-node

import { GetFileContentsSchema } from '../schemas.js';

interface TestResult {
name: string;
status: 'passed' | 'failed';
error?: string;
}

interface SchemaTestCase {
name: string;
input: Record<string, any>;
expected?: {
project_id?: string;
file_path?: string;
ref?: string;
};
shouldFail?: boolean;
}

function runGetFileContentsSchemaTests() {
console.log('🧪 Testing GetFileContentsSchema...');

const cases: SchemaTestCase[] = [
{
name: 'schema:get_file_contents:path-only',
input: { path: 'package.json' },
expected: { file_path: 'package.json', project_id: undefined, ref: undefined }
},
{
name: 'schema:get_file_contents:file-path-precedence',
input: { file_path: ' README.md ', path: 'package.json' },
expected: { file_path: 'README.md', project_id: undefined, ref: undefined }
},
{
name: 'schema:get_file_contents:project-id-trim',
input: { project_id: ' 123 ', file_path: 'a.txt' },
expected: { project_id: '123', file_path: 'a.txt', ref: undefined }
},
{
name: 'schema:get_file_contents:ref-trim-to-undefined',
input: { file_path: 'a.txt', ref: ' ' },
expected: { file_path: 'a.txt', project_id: undefined, ref: undefined }
},
{
name: 'schema:get_file_contents:reject-empty-path',
input: { path: ' ' },
shouldFail: true
}
];

let passed = 0;
let failed = 0;

cases.forEach(testCase => {
const result: TestResult = {
name: testCase.name,
status: 'failed'
};

const parsed = GetFileContentsSchema.safeParse(testCase.input);

if (testCase.shouldFail) {
if (!parsed.success) {
result.status = 'passed';
} else {
result.error = 'Expected schema validation to fail';
}
} else if (parsed.success) {
const { project_id, file_path, ref } = parsed.data;
const expected = testCase.expected || {};
const matches = project_id === expected.project_id && file_path === expected.file_path && ref === expected.ref;
if (matches) {
result.status = 'passed';
} else {
result.error = `Unexpected parsed result: ${JSON.stringify(parsed.data)}`;
}
} else {
result.error = parsed.error?.message || 'Schema validation failed';
}

if (result.status === 'passed') {
passed++;
console.log(`✅ ${result.name}`);
} else {
failed++;
console.log(`❌ ${result.name}: ${result.error}`);
}
});

console.log(`\nResults: ${passed} passed, ${failed} failed`);

if (failed > 0) {
process.exit(1);
}
}

if (import.meta.url === `file://${process.argv[1]}`) {
runGetFileContentsSchemaTests();
}