feat(web): filter GitLab merge requests by source branch
This commit is contained in:
@@ -1290,7 +1290,7 @@ export interface GitLabAPI {
|
|||||||
issueGet(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueGetResult>;
|
issueGet(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueGetResult>;
|
||||||
issueComments(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueCommentsResult>;
|
issueComments(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabIssueCommentsResult>;
|
||||||
|
|
||||||
mrsList(directory: string, options?: { page?: number; query?: string }): Promise<GitLabMergeRequestsListResult>;
|
mrsList(directory: string, options?: { page?: number; query?: string; sourceBranch?: string }): Promise<GitLabMergeRequestsListResult>;
|
||||||
mrContext(
|
mrContext(
|
||||||
directory: string,
|
directory: string,
|
||||||
number: number,
|
number: number,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ Nothing in the client or repo layers assumes the token came from a PAT.
|
|||||||
- Issue list: `GET /projects/:id/issues?state=opened&scope=all&per_page=50&page=N&search=<query>`.
|
- Issue list: `GET /projects/:id/issues?state=opened&scope=all&per_page=50&page=N&search=<query>`.
|
||||||
- Issue detail: `GET /projects/:id/issues/:issue_iid`.
|
- Issue detail: `GET /projects/:id/issues/:issue_iid`.
|
||||||
- Issue notes: `GET /projects/:id/issues/:issue_iid/notes?per_page=100` (system notes are skipped; each note links as `{issue_web_url}#note_{id}`).
|
- Issue notes: `GET /projects/:id/issues/:issue_iid/notes?per_page=100` (system notes are skipped; each note links as `{issue_web_url}#note_{id}`).
|
||||||
- MR list: `GET /projects/:id/merge_requests?state=opened&scope=all&per_page=50&page=N&search=<query>`.
|
- MR list: `GET /projects/:id/merge_requests?state=opened&scope=all&per_page=50&page=N&search=<query>&source_branch=<branch>` (the route passes `sourceBranch` through to `source_branch` when present, matching local-branch MR-status UIs).
|
||||||
- MR detail: `GET /projects/:id/merge_requests/:merge_request_iid`.
|
- MR detail: `GET /projects/:id/merge_requests/:merge_request_iid`.
|
||||||
- MR diffs: `GET /projects/:id/merge_requests/:merge_request_iid/diffs?per_page=100&page=N` (paginated; the route caps at 10 pages / 3000 files).
|
- MR diffs: `GET /projects/:id/merge_requests/:merge_request_iid/diffs?per_page=100&page=N` (paginated; the route caps at 10 pages / 3000 files).
|
||||||
- MR notes: `GET /projects/:id/merge_requests/:merge_request_iid/notes?per_page=100`.
|
- MR notes: `GET /projects/:id/merge_requests/:merge_request_iid/notes?per_page=100`.
|
||||||
@@ -93,7 +93,7 @@ Nothing in the client or repo layers assumes the token came from a PAT.
|
|||||||
| GET | `/api/gitlab/issues/list` | `?directory&page&query` -> `{ connected, repo?, issues[], page, hasMore }` |
|
| GET | `/api/gitlab/issues/list` | `?directory&page&query` -> `{ connected, repo?, issues[], page, hasMore }` |
|
||||||
| GET | `/api/gitlab/issues/get` | `?directory&number&namespace&project` -> `{ connected, repo?, issue }` |
|
| GET | `/api/gitlab/issues/get` | `?directory&number&namespace&project` -> `{ connected, repo?, issue }` |
|
||||||
| GET | `/api/gitlab/issues/comments` | `?directory&number&namespace&project` -> `{ connected, repo?, comments[] }` |
|
| GET | `/api/gitlab/issues/comments` | `?directory&number&namespace&project` -> `{ connected, repo?, comments[] }` |
|
||||||
| GET | `/api/gitlab/mrs/list` | `?directory&page&query` -> `{ connected, repo?, mrs[], page, hasMore }` |
|
| GET | `/api/gitlab/mrs/list` | `?directory&page&query&sourceBranch` -> `{ connected, repo?, mrs[], page, hasMore }` |
|
||||||
| GET | `/api/gitlab/mrs/context` | `?directory&number&diff&namespace&project` -> `{ connected, repo?, mr, comments[], files[], diff? }` |
|
| GET | `/api/gitlab/mrs/context` | `?directory&number&diff&namespace&project` -> `{ connected, repo?, mr, comments[], files[], diff? }` |
|
||||||
| GET | `/api/gitlab/repo/branches` | `?namespace&project` -> `{ branches[] }` |
|
| GET | `/api/gitlab/repo/branches` | `?namespace&project` -> `{ branches[] }` |
|
||||||
|
|
||||||
|
|||||||
@@ -507,6 +507,7 @@ export function registerGitLabRoutes(app, options = {}) {
|
|||||||
const rawPage = typeof req.query?.page === 'string' ? Number(req.query.page) : 1;
|
const rawPage = typeof req.query?.page === 'string' ? Number(req.query.page) : 1;
|
||||||
const effectivePage = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
|
const effectivePage = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
|
||||||
const searchQuery = asString(req.query?.query);
|
const searchQuery = asString(req.query?.query);
|
||||||
|
const sourceBranch = asString(req.query?.sourceBranch);
|
||||||
|
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@@ -522,6 +523,9 @@ export function registerGitLabRoutes(app, options = {}) {
|
|||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
params.search = searchQuery;
|
params.search = searchQuery;
|
||||||
}
|
}
|
||||||
|
if (sourceBranch) {
|
||||||
|
params.source_branch = sourceBranch;
|
||||||
|
}
|
||||||
const resp = await withTimeout(client.mergeRequests(projectPath, params), ROUTE_TIMEOUT_MS, 'gitlab mrs list');
|
const resp = await withTimeout(client.mergeRequests(projectPath, params), ROUTE_TIMEOUT_MS, 'gitlab mrs list');
|
||||||
if (resp.status === 429) {
|
if (resp.status === 429) {
|
||||||
return res.status(503).json({ error: 'GitLab rate limited' });
|
return res.status(503).json({ error: 'GitLab rate limited' });
|
||||||
|
|||||||
@@ -396,6 +396,28 @@ describe('GitLab data routes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mrs/list passes the source branch filter to the GitLab API', async () => {
|
||||||
|
const fetchMock = scriptedFetch([(url) => (matches(/\/merge_requests\?/)(url) ? jsonResponse([]) : null)]);
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
await request(app).get('/api/gitlab/mrs/list?directory=%2Ftmp%2Fwork&sourceBranch=feat%2Fapi');
|
||||||
|
|
||||||
|
const requestedUrl = String(fetchMock.mock.calls[0][0]);
|
||||||
|
expect(requestedUrl).toContain('state=opened');
|
||||||
|
expect(requestedUrl).toContain('source_branch=feat%2Fapi');
|
||||||
|
expect(requestedUrl).toContain('per_page=50');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mrs/list omits the source branch filter when not provided', async () => {
|
||||||
|
const fetchMock = scriptedFetch([(url) => (matches(/\/merge_requests\?/)(url) ? jsonResponse([]) : null)]);
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
await request(app).get('/api/gitlab/mrs/list?directory=%2Ftmp%2Fwork');
|
||||||
|
|
||||||
|
const requestedUrl = String(fetchMock.mock.calls[0][0]);
|
||||||
|
expect(requestedUrl).not.toContain('source_branch');
|
||||||
|
});
|
||||||
|
|
||||||
test('mrs/context returns mr, comments, files, and a concatenated diff', async () => {
|
test('mrs/context returns mr, comments, files, and a concatenated diff', async () => {
|
||||||
scriptedFetch([
|
scriptedFetch([
|
||||||
(url) => (matches(/\/merge_requests\/9$/)(url)
|
(url) => (matches(/\/merge_requests\/9$/)(url)
|
||||||
|
|||||||
@@ -105,6 +105,46 @@ describe('createWebGitLabAPI', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes sourceBranch query param to mrsList', async () => {
|
||||||
|
const result = {
|
||||||
|
connected: true,
|
||||||
|
repo: null,
|
||||||
|
mrs: [],
|
||||||
|
page: 1,
|
||||||
|
hasMore: false,
|
||||||
|
};
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json(result));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.mrsList('/workspace', { page: 1, query: 'search', sourceBranch: 'feat/api' })).resolves.toEqual(result);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ directory: '/workspace', page: '1', query: 'search', sourceBranch: 'feat/api' });
|
||||||
|
expect(runtimeFetchMock).toHaveBeenCalledWith(`/api/gitlab/mrs/list?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits sourceBranch when not provided to mrsList', async () => {
|
||||||
|
const result = {
|
||||||
|
connected: true,
|
||||||
|
repo: null,
|
||||||
|
mrs: [],
|
||||||
|
page: 1,
|
||||||
|
hasMore: false,
|
||||||
|
};
|
||||||
|
runtimeFetchMock.mockResolvedValueOnce(Response.json(result));
|
||||||
|
|
||||||
|
const api = await createAPI();
|
||||||
|
await expect(api.mrsList('/workspace', { page: 1 })).resolves.toEqual(result);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ directory: '/workspace', page: '1' });
|
||||||
|
expect(runtimeFetchMock).toHaveBeenCalledWith(`/api/gitlab/mrs/list?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('throws the server error message on {error} payloads', async () => {
|
it('throws the server error message on {error} payloads', async () => {
|
||||||
runtimeFetchMock.mockResolvedValueOnce(Response.json({ error: 'Not connected to GitLab' }, { status: 401 }));
|
runtimeFetchMock.mockResolvedValueOnce(Response.json({ error: 'Not connected to GitLab' }, { status: 401 }));
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI =>
|
|||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
async mrsList(directory: string, options?: { page?: number; query?: string }): Promise<GitLabMergeRequestsListResult> {
|
async mrsList(directory: string, options?: { page?: number; query?: string; sourceBranch?: string }): Promise<GitLabMergeRequestsListResult> {
|
||||||
const page = options?.page ?? 1;
|
const page = options?.page ?? 1;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
directory,
|
directory,
|
||||||
@@ -138,6 +138,9 @@ export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI =>
|
|||||||
if (options?.query) {
|
if (options?.query) {
|
||||||
params.set('query', options.query);
|
params.set('query', options.query);
|
||||||
}
|
}
|
||||||
|
if (options?.sourceBranch) {
|
||||||
|
params.set('sourceBranch', options.sourceBranch);
|
||||||
|
}
|
||||||
const response = await runtimeFetch(
|
const response = await runtimeFetch(
|
||||||
`/api/gitlab/mrs/list?${params.toString()}`,
|
`/api/gitlab/mrs/list?${params.toString()}`,
|
||||||
{ method: 'GET', headers: { Accept: 'application/json' } }
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
||||||
|
|||||||
Reference in New Issue
Block a user