import fetch from "node-fetch";
import { z } from 'zod';
// src
import { GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_API_URL } from './constants.js';
import {
GitLabForkSchema,
GitLabReferenceSchema,
GitLabRepositorySchema,
GitLabIssueSchema,
GitLabMergeRequestSchema,
GitLabContentSchema,
GitLabCreateUpdateFileResponseSchema,
GitLabSearchResponseSchema,
GitLabTreeSchema,
GitLabCommitSchema,
CreateRepositoryOptionsSchema,
CreateIssueOptionsSchema,
CreateMergeRequestOptionsSchema,
CreateBranchOptionsSchema,
type GitLabFork,
type GitLabReference,
type GitLabRepository,
type GitLabIssue,
type GitLabMergeRequest,
type GitLabContent,
type GitLabCreateUpdateFileResponse,
type GitLabSearchResponse,
type GitLabTree,
type GitLabCommit,
type FileOperation,
} from '../../schemas/index.js';
// each function gets the params, some are optional
// each returns a primse or specific type
export async function forkProject(
projectId: string,
namespace?: string
): Promise<GitLabFork> {
const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`;
const queryParams = namespace ? `?namespace=${encodeURIComponent(namespace)}` : '';
const response = await fetch(url + queryParams, {
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
// parse the response
return GitLabForkSchema.parse(await response.json());
}
export async function createBranch(
projectId: string,
options: z.infer<typeof CreateBranchOptionsSchema>
): Promise<GitLabReference> {
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
// for body, use stringification
body: JSON.stringify({
branch: options.name,
ref: options.ref
})
}
);
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabReferenceSchema.parse(await response.json());
}
export async function getFileContents(
projectId: string,
filePath: string,
ref?: string
): Promise<GitLabContent> {
const encodedPath = encodeURIComponent(filePath);
let url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`;
if (ref) {
url += `?ref=${encodeURIComponent(ref)}`;
} else {
url += '?ref=HEAD';
}
const response = await fetch(url, {
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
const data = GitLabContentSchema.parse(await response.json());
if (!Array.isArray(data) && data.content) {
data.content = Buffer.from(data.content, 'base64').toString('utf8');
}
return data;
}
export async function createIssue(
projectId: string,
options: z.infer<typeof CreateIssueOptionsSchema>
): Promise<GitLabIssue> {
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
title: options.title,
description: options.description,
assignee_ids: options.assignee_ids,
milestone_id: options.milestone_id,
labels: options.labels?.join(',')
})
}
);
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabIssueSchema.parse(await response.json());
}
export async function createMergeRequest(
projectId: string,
options: z.infer<typeof CreateMergeRequestOptionsSchema>
): Promise<GitLabMergeRequest> {
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
title: options.title,
description: options.description,
source_branch: options.source_branch,
target_branch: options.target_branch,
allow_collaboration: options.allow_collaboration,
draft: options.draft
})
}
);
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabMergeRequestSchema.parse(await response.json());
}
export async function createOrUpdateFile(
projectId: string,
filePath: string,
content: string,
commitMessage: string,
branch: string,
previousPath?: string
): Promise<GitLabCreateUpdateFileResponse> {
const encodedPath = encodeURIComponent(filePath);
const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`;
const body = {
branch,
content,
commit_message: commitMessage,
...(previousPath ? { previous_path: previousPath } : {})
};
// Check if file exists
let method = "POST";
try {
await getFileContents(projectId, filePath, branch);
method = "PUT";
} catch (error) {
// File doesn't exist, use POST
}
const response = await fetch(url, {
method,
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabCreateUpdateFileResponseSchema.parse(await response.json());
}
export async function createTree(
projectId: string,
files: FileOperation[],
ref?: string
): Promise<GitLabTree> {
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
files: files.map(file => ({
file_path: file.path,
content: file.content
})),
...(ref ? { ref } : {})
})
}
);
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabTreeSchema.parse(await response.json());
}
export async function createCommit(
projectId: string,
message: string,
branch: string,
actions: FileOperation[]
): Promise<GitLabCommit> {
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
branch,
commit_message: message,
actions: actions.map(action => ({
action: "create",
file_path: action.path,
content: action.content
}))
})
}
);
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabCommitSchema.parse(await response.json());
}
export async function searchProjects(
query: string,
page: number = 1,
perPage: number = 20
): Promise<GitLabSearchResponse> {
const url = new URL(`${GITLAB_API_URL}/projects`);
url.searchParams.append("search", query);
url.searchParams.append("page", page.toString());
url.searchParams.append("per_page", perPage.toString());
const response = await fetch(url.toString(), {
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
const projects = await response.json();
return GitLabSearchResponseSchema.parse({
count: parseInt(response.headers.get("X-Total") || "0"),
items: projects
});
}
export async function createRepository(
options: z.infer<typeof CreateRepositoryOptionsSchema>
): Promise<GitLabRepository> {
const response = await fetch(`${GITLAB_API_URL}/projects`, {
method: "POST",
headers: {
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: options.name,
description: options.description,
visibility: options.visibility,
initialize_with_readme: options.initialize_with_readme
})
});
if (!response.ok) {
throw new Error(`GitLab API error: ${response.statusText}`);
}
return GitLabRepositorySchema.parse(await response.json());
}