feat(web): add GitLab API client wrapper and types
This commit is contained in:
@@ -1136,6 +1136,170 @@ export interface GitHubAPI {
|
|||||||
repoBranches(owner: string, repo: string): Promise<string[]>;
|
repoBranches(owner: string, repo: string): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GitLabUserSummary = {
|
||||||
|
username: string;
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
webUrl?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GitLabRepoRef = {
|
||||||
|
namespace: string;
|
||||||
|
project: string;
|
||||||
|
host: string;
|
||||||
|
url: string;
|
||||||
|
baseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabIssueSummary = {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
state: string;
|
||||||
|
author: GitLabUserSummary;
|
||||||
|
labels: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabIssue = {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
state: string;
|
||||||
|
body?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
author: GitLabUserSummary;
|
||||||
|
assignees?: GitLabUserSummary[];
|
||||||
|
labels: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabIssueComment = {
|
||||||
|
id: number;
|
||||||
|
url: string;
|
||||||
|
body: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
author: GitLabUserSummary;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabIssuesListResult = {
|
||||||
|
connected: boolean;
|
||||||
|
repo?: GitLabRepoRef | null;
|
||||||
|
issues: GitLabIssueSummary[];
|
||||||
|
page: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabIssueGetResult = {
|
||||||
|
connected: boolean;
|
||||||
|
repo?: GitLabRepoRef | null;
|
||||||
|
issue?: GitLabIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabIssueCommentsResult = {
|
||||||
|
connected: boolean;
|
||||||
|
repo?: GitLabRepoRef | null;
|
||||||
|
comments: GitLabIssueComment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabMergeRequestSummary = {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
state: string;
|
||||||
|
draft: boolean;
|
||||||
|
author: GitLabUserSummary;
|
||||||
|
sourceBranch: string;
|
||||||
|
targetBranch: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabMergeRequest = {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
state: string;
|
||||||
|
draft: boolean;
|
||||||
|
body?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
author: GitLabUserSummary;
|
||||||
|
sourceBranch: string;
|
||||||
|
targetBranch: string;
|
||||||
|
headSha?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GitLabMergeRequestFile = {
|
||||||
|
filename: string;
|
||||||
|
status?: string;
|
||||||
|
additions?: number;
|
||||||
|
deletions?: number;
|
||||||
|
changes?: number;
|
||||||
|
patch?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabMergeRequestsListResult = {
|
||||||
|
connected: boolean;
|
||||||
|
repo?: GitLabRepoRef | null;
|
||||||
|
mrs: GitLabMergeRequestSummary[];
|
||||||
|
page: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabMergeRequestContextResult = {
|
||||||
|
connected: boolean;
|
||||||
|
repo?: GitLabRepoRef | null;
|
||||||
|
mr?: GitLabMergeRequest;
|
||||||
|
comments?: GitLabIssueComment[];
|
||||||
|
files?: GitLabMergeRequestFile[];
|
||||||
|
diff?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabBranchesResult = {
|
||||||
|
branches: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type GitLabAuthAccount = {
|
||||||
|
id: string;
|
||||||
|
user: {
|
||||||
|
username: string;
|
||||||
|
name?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
webUrl?: string;
|
||||||
|
};
|
||||||
|
baseUrl: string;
|
||||||
|
current: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitLabAuthStatus = {
|
||||||
|
connected: boolean;
|
||||||
|
user?: GitLabUserSummary;
|
||||||
|
accounts: GitLabAuthAccount[];
|
||||||
|
defaultBaseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GitLabAPI {
|
||||||
|
authStatus(): Promise<GitLabAuthStatus>;
|
||||||
|
authConnect(input: { accessToken: string; baseUrl?: string }): Promise<GitLabAuthStatus>;
|
||||||
|
authActivate(accountId: string): Promise<GitLabAuthStatus>;
|
||||||
|
authDisconnect(): Promise<{ removed: boolean }>;
|
||||||
|
me(): Promise<GitLabUserSummary>;
|
||||||
|
|
||||||
|
issuesList(directory: string, options?: { page?: number; query?: string }): Promise<GitLabIssuesListResult>;
|
||||||
|
issueGet(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueGetResult>;
|
||||||
|
issueComments(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueCommentsResult>;
|
||||||
|
|
||||||
|
mrsList(directory: string, options?: { page?: number; query?: string }): Promise<GitLabMergeRequestsListResult>;
|
||||||
|
mrContext(
|
||||||
|
directory: string,
|
||||||
|
number: number,
|
||||||
|
options?: { includeDiff?: boolean; namespace?: string; project?: string }
|
||||||
|
): Promise<GitLabMergeRequestContextResult>;
|
||||||
|
|
||||||
|
repoBranches(namespace: string, project: string): Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RemoteClientRecord {
|
export interface RemoteClientRecord {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -1231,6 +1395,7 @@ export interface RuntimeAPIs {
|
|||||||
permissions: PermissionsAPI;
|
permissions: PermissionsAPI;
|
||||||
notifications: NotificationsAPI;
|
notifications: NotificationsAPI;
|
||||||
github?: GitHubAPI;
|
github?: GitHubAPI;
|
||||||
|
gitlab?: GitLabAPI;
|
||||||
push?: PushAPI;
|
push?: PushAPI;
|
||||||
diagnostics?: DiagnosticsAPI;
|
diagnostics?: DiagnosticsAPI;
|
||||||
clientAuth?: ClientAuthAPI;
|
clientAuth?: ClientAuthAPI;
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { RuntimeUrlQuery, RuntimeUrlResolver } from '@openchamber/ui/lib/runtime-url';
|
||||||
|
|
||||||
|
const runtimeFetchMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@openchamber/ui/lib/runtime-fetch', () => ({
|
||||||
|
runtimeFetch: runtimeFetchMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const toUrl = (path: string, query?: RuntimeUrlQuery): string => {
|
||||||
|
const params = query instanceof URLSearchParams ? query : new URLSearchParams();
|
||||||
|
const queryString = params.toString();
|
||||||
|
return queryString ? `${path}?${queryString}` : path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const urls: RuntimeUrlResolver = {
|
||||||
|
api: toUrl,
|
||||||
|
authenticatedAsset: toUrl,
|
||||||
|
auth: toUrl,
|
||||||
|
health: (query?: RuntimeUrlQuery) => toUrl('/health', query),
|
||||||
|
rawFile: (path: string) => toUrl('/api/fs/raw', new URLSearchParams({ path })),
|
||||||
|
sse: toUrl,
|
||||||
|
websocket: toUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAPI = async () => {
|
||||||
|
const { createWebGitLabAPI } = await import('./gitlab');
|
||||||
|
return createWebGitLabAPI({ urls });
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
runtimeFetchMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createWebGitLabAPI', () => {
|
||||||
|
it('parses auth status payloads', async () => {
|
||||||
|
const status = {
|
||||||
|
connected: true,
|
||||||
|
user: { username: 'octocat', id: 1, name: 'Octo Cat' },
|
||||||
|
accounts: [],
|
||||||
|
defaultBaseUrl: 'https://gitlab.com',
|
||||||
|
};
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json(status));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.authStatus()).resolves.toEqual(status);
|
||||||
|
|
||||||
|
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitlab/auth/status', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends the access token and base URL when connecting', async () => {
|
||||||
|
const status = {
|
||||||
|
connected: true,
|
||||||
|
user: { username: 'octocat', id: 1 },
|
||||||
|
accounts: [],
|
||||||
|
defaultBaseUrl: 'https://gitlab.com',
|
||||||
|
};
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json(status));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.authConnect({ accessToken: 'glpat-123', baseUrl: 'https://gitlab.example' })).resolves.toEqual(status);
|
||||||
|
|
||||||
|
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitlab/auth/connect', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
body: JSON.stringify({ accessToken: 'glpat-123', baseUrl: 'https://gitlab.example' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes directory, number, namespace and project query params to issueGet', async () => {
|
||||||
|
const result = {
|
||||||
|
connected: true,
|
||||||
|
repo: null,
|
||||||
|
issue: { number: 42, title: 'Broken build', url: 'https://gitlab.com/g/repo/-/issues/42', state: 'opened', author: { username: 'octocat', id: 1 }, labels: [] },
|
||||||
|
};
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json(result));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.issueGet('/workspace', 42, { namespace: 'group', project: 'repo' })).resolves.toEqual(result);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ directory: '/workspace', number: '42', namespace: 'group', project: 'repo' });
|
||||||
|
expect(runtimeFetchMock).toHaveBeenCalledWith(`/api/gitlab/issues/get?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes diff=1 and repo params to mrContext', async () => {
|
||||||
|
const result = { connected: true, repo: null, mr: undefined };
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json(result));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.mrContext('/workspace', 7, { includeDiff: true, namespace: 'group', project: 'repo' })).resolves.toEqual(result);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ directory: '/workspace', number: '7', diff: '1', namespace: 'group', project: 'repo' });
|
||||||
|
expect(runtimeFetchMock).toHaveBeenCalledWith(`/api/gitlab/mrs/context?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws the server error message on {error} payloads', async () => {
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json({ error: 'Not connected to GitLab' }, { status: 401 }));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.authStatus()).rejects.toThrow('Not connected to GitLab');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws the response status text when no error payload is present', async () => {
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json({}, { status: 500, statusText: 'Internal Server Error' }));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.issuesList('/workspace')).rejects.toThrow('Internal Server Error');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import type {
|
||||||
|
GitLabAPI,
|
||||||
|
GitLabAuthStatus,
|
||||||
|
GitLabBranchesResult,
|
||||||
|
GitLabIssueCommentsResult,
|
||||||
|
GitLabIssueGetResult,
|
||||||
|
GitLabIssuesListResult,
|
||||||
|
GitLabMergeRequestContextResult,
|
||||||
|
GitLabMergeRequestsListResult,
|
||||||
|
GitLabUserSummary,
|
||||||
|
} from '@openchamber/ui/lib/api/types';
|
||||||
|
import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch';
|
||||||
|
import type { RuntimeUrlResolver } from '@openchamber/ui/lib/runtime-url';
|
||||||
|
|
||||||
|
interface WebGitLabAPIOptions {
|
||||||
|
urls: RuntimeUrlResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonOrNull = async <T>(response: Response): Promise<T | null> => {
|
||||||
|
return (await response.json().catch(() => null)) as T | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI => ({
|
||||||
|
async authStatus(): Promise<GitLabAuthStatus> {
|
||||||
|
const response = await runtimeFetch('/api/gitlab/auth/status', { method: 'GET', headers: { Accept: 'application/json' } });
|
||||||
|
const payload = await jsonOrNull<GitLabAuthStatus & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab status');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async authConnect(input: { accessToken: string; baseUrl?: string }): Promise<GitLabAuthStatus> {
|
||||||
|
const response = await runtimeFetch('/api/gitlab/auth/connect', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
const payload = await jsonOrNull<GitLabAuthStatus & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to connect GitLab');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async authActivate(accountId: string): Promise<GitLabAuthStatus> {
|
||||||
|
const response = await runtimeFetch('/api/gitlab/auth/activate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
body: JSON.stringify({ accountId }),
|
||||||
|
});
|
||||||
|
const payload = await jsonOrNull<GitLabAuthStatus & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to activate GitLab account');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async authDisconnect(): Promise<{ removed: boolean }> {
|
||||||
|
const response = await runtimeFetch('/api/gitlab/auth', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
const payload = await jsonOrNull<{ removed?: boolean; error?: string }>(response);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to disconnect GitLab');
|
||||||
|
}
|
||||||
|
return { removed: Boolean(payload?.removed) };
|
||||||
|
},
|
||||||
|
|
||||||
|
async me(): Promise<GitLabUserSummary> {
|
||||||
|
const response = await runtimeFetch('/api/gitlab/me', { method: 'GET', headers: { Accept: 'application/json' } });
|
||||||
|
const payload = await jsonOrNull<GitLabUserSummary & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to fetch GitLab user');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async issuesList(directory: string, options?: { page?: number; query?: string }): Promise<GitLabIssuesListResult> {
|
||||||
|
const page = options?.page ?? 1;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
directory,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
if (options?.query) {
|
||||||
|
params.set('query', options.query);
|
||||||
|
}
|
||||||
|
const response = await runtimeFetch(
|
||||||
|
`/api/gitlab/issues/list?${params.toString()}`,
|
||||||
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
||||||
|
);
|
||||||
|
const payload = await jsonOrNull<GitLabIssuesListResult & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab issues');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async issueGet(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueGetResult> {
|
||||||
|
const params = new URLSearchParams({ directory, number: String(number) });
|
||||||
|
if (options?.namespace) {
|
||||||
|
params.set('namespace', options.namespace);
|
||||||
|
}
|
||||||
|
if (options?.project) {
|
||||||
|
params.set('project', options.project);
|
||||||
|
}
|
||||||
|
const response = await runtimeFetch(urls.api('/api/gitlab/issues/get', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
||||||
|
const payload = await jsonOrNull<GitLabIssueGetResult & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab issue');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async issueComments(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueCommentsResult> {
|
||||||
|
const params = new URLSearchParams({ directory, number: String(number) });
|
||||||
|
if (options?.namespace) {
|
||||||
|
params.set('namespace', options.namespace);
|
||||||
|
}
|
||||||
|
if (options?.project) {
|
||||||
|
params.set('project', options.project);
|
||||||
|
}
|
||||||
|
const response = await runtimeFetch(urls.api('/api/gitlab/issues/comments', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
||||||
|
const payload = await jsonOrNull<GitLabIssueCommentsResult & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab issue comments');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async mrsList(directory: string, options?: { page?: number; query?: string }): Promise<GitLabMergeRequestsListResult> {
|
||||||
|
const page = options?.page ?? 1;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
directory,
|
||||||
|
page: String(page),
|
||||||
|
});
|
||||||
|
if (options?.query) {
|
||||||
|
params.set('query', options.query);
|
||||||
|
}
|
||||||
|
const response = await runtimeFetch(
|
||||||
|
`/api/gitlab/mrs/list?${params.toString()}`,
|
||||||
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
||||||
|
);
|
||||||
|
const payload = await jsonOrNull<GitLabMergeRequestsListResult & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge requests');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async mrContext(
|
||||||
|
directory: string,
|
||||||
|
number: number,
|
||||||
|
options?: { includeDiff?: boolean; namespace?: string; project?: string }
|
||||||
|
): Promise<GitLabMergeRequestContextResult> {
|
||||||
|
const params = new URLSearchParams({ directory, number: String(number) });
|
||||||
|
if (options?.includeDiff) {
|
||||||
|
params.set('diff', '1');
|
||||||
|
}
|
||||||
|
if (options?.namespace) {
|
||||||
|
params.set('namespace', options.namespace);
|
||||||
|
}
|
||||||
|
if (options?.project) {
|
||||||
|
params.set('project', options.project);
|
||||||
|
}
|
||||||
|
const response = await runtimeFetch(urls.api('/api/gitlab/mrs/context', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
||||||
|
const payload = await jsonOrNull<GitLabMergeRequestContextResult & { error?: string }>(response);
|
||||||
|
if (!response.ok || !payload) {
|
||||||
|
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge request context');
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
async repoBranches(namespace: string, project: string): Promise<string[]> {
|
||||||
|
const response = await runtimeFetch(
|
||||||
|
`/api/gitlab/repo/branches?namespace=${encodeURIComponent(namespace)}&project=${encodeURIComponent(project)}`,
|
||||||
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
||||||
|
);
|
||||||
|
const body = await jsonOrNull<GitLabBranchesResult & { error?: string }>(response);
|
||||||
|
if (!response.ok || !body) {
|
||||||
|
throw new Error(body?.error || response.statusText || 'Failed to fetch GitLab repo branches');
|
||||||
|
}
|
||||||
|
return body.branches ?? [];
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@ import { createWebNotificationsAPI } from './notifications';
|
|||||||
import { createWebToolsAPI } from './tools';
|
import { createWebToolsAPI } from './tools';
|
||||||
import { createWebPushAPI } from './push';
|
import { createWebPushAPI } from './push';
|
||||||
import { createWebGitHubAPI } from './github';
|
import { createWebGitHubAPI } from './github';
|
||||||
|
import { createWebGitLabAPI } from './gitlab';
|
||||||
import { createWebClientAuthAPI } from './clientAuth';
|
import { createWebClientAuthAPI } from './clientAuth';
|
||||||
|
|
||||||
export interface WebAPIsOptions {
|
export interface WebAPIsOptions {
|
||||||
@@ -45,6 +46,7 @@ export const createWebAPIs = (options: WebAPIsOptions = {}): RuntimeAPIs => {
|
|||||||
permissions: createWebPermissionsAPI(),
|
permissions: createWebPermissionsAPI(),
|
||||||
notifications: createWebNotificationsAPI(),
|
notifications: createWebNotificationsAPI(),
|
||||||
github: createWebGitHubAPI({ urls: activeUrls }),
|
github: createWebGitHubAPI({ urls: activeUrls }),
|
||||||
|
gitlab: createWebGitLabAPI({ urls: activeUrls }),
|
||||||
push: createWebPushAPI(),
|
push: createWebPushAPI(),
|
||||||
clientAuth: createWebClientAuthAPI(),
|
clientAuth: createWebClientAuthAPI(),
|
||||||
tools: createWebToolsAPI(),
|
tools: createWebToolsAPI(),
|
||||||
|
|||||||
Reference in New Issue
Block a user