dfaab11959
* feat(backend): full GitLab parity — store, service, poller, WS, presets Add the watcher/poll/write-action surface on top of the existing GitLab client + service skeleton: * 5 new SQLite tables (mr_watches, review_watches, review_mr_tasks, issue_watches, issue_watch_tasks, action_presets) + CRUD * Client + Service: MergeMR, GetProjectMergeMethods, GetProtectedBranch, ListUserProjects, SearchProjects, SetMRLabels, SetMRAssignees * Service: review/issue/MR watch CRUD + Check/Trigger, GetStats, CleanupAllReview/IssueTasks (cleanup-policy aware), TriggerMRSync, action-presets CRUD with defaults * Background poller (MR / review / issue loops) wired from main.go * ~35 GitLab WS action constants + handlers.go registrations * Mock controller for E2E seeding (MRs, issues, pipelines, discussions, approvals, branches) * eventBus + taskDeleter + taskSessionChecker plumbed through Service * Event types: GitLabMRFeedback / NewReviewMR / NewIssue / TaskMRUpdated * feat(web): GitLab frontend parity — types, API, store slice, domain hooks Extend the frontend GitLab integration on top of the new backend surface: * HTTP API client expansion (gitlab-api.ts): full CRUD for review/issue/MR watches, MR write actions (merge/approve/labels/assignees), MR files/commits/feedback, action presets, projects autocomplete, stats * Store slice expansion: review/issue/MR watches lists, action presets by workspace, stats + status with loading flags. Properties prefixed with gitlab to avoid collision with GitHub slice * Domain hooks: use-gitlab-status, use-gitlab-stats, use-gitlab-review-watches, use-gitlab-issue-watches, use-gitlab-action-presets — mirror github hook shapes * Type model: ReviewWatch, IssueWatch, MRWatch, ActionPresets, Stats, ProjectMergeMethods, MRApproval, Pipeline, MRFeedback, etc. * Backend controller HTTP routes for watches/presets/write-actions/ projects/stats so the frontend HTTP API client has endpoints to hit All passes: backend (build + vet + lint + tests), web (typecheck + lint + 2400 tests). * fix(gitlab): wire orchestrator event handlers, fix label-filter drop, add tests Address Claude review blockers on the parity PR: 1. Wire orchestrator subscriptions for GitLab review/issue watch events. Add event_handlers_gitlab.go with handleGitLabNewReviewMR / handleGitLabNewIssue, dedup reservation handshake, and task-creator interfaces (mirrors event_handlers_github.go). main.go now calls orchestratorSvc.SetGitLabService so the dedup APIs are usable 2. Fix silent label-filter drop: when a custom_query is set on an issue watch, labels were being appended to the unused filter arg. Fold labels into customQuery (or default filter) so they actually reach GitLab; extracted appendLabelsToQuery helper with explicit handling of pre-existing labels= clauses 3. Switch event payloads to pointer types (mirrors GitHub) so handlers can type-assert pointer events 4. Backend tests: store_watches_test.go (CRUD + reserve/assign for all 4 new tables + presets), service_watches_test.go (label-filter helper, cleanup policy, project normalisation), action_presets_test.go 5. Frontend slice tests: review/issue watch CRUD round-trips, action presets + stats reducer coverage All green: backend (build+vet+lint+tests), web (typecheck+lint+2403 tests). * fix(gitlab): address remaining blockers — cleanup wiring, safe defaults, nil guards Address the second round of Claude review blockers: 1. main.go: wire SetTaskDeleter + SetTaskSessionChecker on services.GitLab (mirrors GitHub). Without these, manual cleanup sweeps would always error with "task deleter not configured" 2. service_cleanup.go: default cleanup policy on transient DB error is now CleanupPolicyNever (preserve tasks) instead of CleanupPolicyAuto (silently delete). Genuine "watch was deleted" path also falls under the safe-side default; user can manually delete the orphan tasks 3. service_watches.go: add nil guards on requireStore() so the 6 MR-watch list/get/delete methods return an errStoreUnavailable error instead of panicking when the SQLite store failed to construct at boot 4. use-gitlab-status.ts + use-gitlab-stats.ts: add per-mount attemptedRef so an unreachable GitLab doesn't trigger an infinite re-fetch loop — the previous useEffect re-ran every render because the failure path left status null, satisfying the !status guard * fix(gitlab): propagate session-check errors, clamp poll interval on update Address remaining Claude blockers: * service_cleanup.go: HasUserAuthoredMessage transient errors are now preserved (return false → skip delete) instead of silently ignored; the alternative was occasionally deleting tasks a user had touched * service_watches.go: extract clampPollInterval helper applying the same bounds (0 → default, <60 → 60) as the create path; both applyReviewWatchPatch and applyIssueWatchPatch use it so user-supplied zero or tiny values via UpdateXxxWatchRequest no longer hammer GitLab * fix(gitlab): clear dupl lint, fix preset retry loop + per-workspace watch cache * service_watches.go: applyReviewWatchPatch / applyIssueWatchPatch get nolint:dupl markers — they share shape but per-domain field validation lives in the create paths, so deduplicating via generics would obscure the contract. Restores backend lint to 0 issues * use-gitlab-action-presets.ts: per-workspace attemptedRef set so a failing preset fetch doesn't retry on every render * use-gitlab-review-watches.ts + use-gitlab-issue-watches.ts: track lastFetchedRef per consumer so a workspace switch triggers a refetch. The slice-level loaded flag is shared across instances and can't double as a per-workspace cache key * fix(gitlab): add nil-store guards across review/issue watch + cleanup paths Round-3 Claude finding: ReviewWatch / IssueWatch / preset / reservation methods still called s.requireStore().X() directly. If NewStore fails at boot (table migration error), the service struct's store is nil and every list/get/update/create/reserve method panics on first request. Apply the same store-nil → errStoreUnavailable pattern from f95a929 to every remaining method: * CreateReviewWatch / CreateIssueWatch * GetReviewWatch / ListReviewWatches / ListAllReviewWatches / UpdateReviewWatch / TriggerReviewWatchAll * GetIssueWatch / ListIssueWatches / ListAllIssueWatches / UpdateIssueWatch / TriggerIssueWatchAll * ReserveReviewMRTask / AssignReviewMRTaskID / ReleaseReviewMRTask * ReserveIssueWatchTask / AssignIssueWatchTaskID / ReleaseIssueWatchTask * lookupReviewPolicy / lookupIssuePolicy (return CleanupPolicyNever) * fix(gitlab): nil-store guards on CheckXxxWatch + DeleteXxxWatch + cleanup paths Round-4 Claude finding: 4 remaining panic sites the previous nil-guard pass missed — these run from the background poller (every 5 min) and the manual delete/cleanup flows, so a boot-time NewStore failure would crash the orchestrator process on first tick. * service_watches.go: add store-nil guards to DeleteReviewWatch / DeleteIssueWatch / CheckReviewWatch / CheckIssueWatch * service_cleanup.go: guard CleanupAllReviewTasks / CleanupAllIssueTasks entry points; wrap the trailing DeleteReviewMRTask / DeleteIssueWatchTask calls in `if store := s.requireStore(); store != nil` blocks so the cleanup succeeds even when the dedup-row delete is unreachable * refactor(gitlab): split service_watches.go to stay under 800-line revive limit CI's --new-from-rev lint caught service_watches.go at 808 lines (limit 800). Split into three focused files: * service_events.go (94 LOC) — publish helpers for MR feedback / new review MR / new issue / watch lifecycle events * service_issue_watches.go (302 LOC) — Issue watch CRUD + Check + Trigger + fetch + helpers * service_reservations.go (59 LOC) — Reserve/Assign/Release dedup handles used by the orchestrator event handlers service_watches.go is now 505 lines and contains MR watch + Review watch only. No behavioral change. * fix(gitlab): address inline code-review feedback Address actionable inline comments from Claude/CodeRabbit/cubic/Greptile: * appendLabelsToQuery: use url.ParseQuery for exact key match instead of strings.Contains; previously false-matched keys like mylabels= and silently dropped the watch's labels (test added) * fetchReviewMRs: drop dead `filter = watch.CustomQuery` assignment — SearchMRs's buildMRSearchQuery returns customQuery verbatim and ignores filter when customQuery is non-empty * store.go: add workspace_id indexes on gitlab_review_watches and gitlab_issue_watches (the 5-min poller and HTTP list endpoints did full table scans) * mock_client.SearchProjects: switch from == to case-insensitive Contains so partial-query autocomplete returns the seeded project, matching the doc comment * Poller: add sync.Mutex to guard `started` field; Start/Stop are now safe to call concurrently (go test -race would have flagged the previous read/write race) * Add ErrWatchNotFound sentinel; controller_watches.go maps it to HTTP 404 via httpRespondError helper. Update/Trigger handlers previously returned 500 for missing watches, hiding the distinction from real server faults * service_events.go: log Publish errors on watch lifecycle events instead of swallowing them silently * handlers.go: reject malformed JSON payloads in list-style WS handlers rather than silently falling through to broader list/search behavior * event_handlers_gitlab.go: remove dead gitlabRepoSlug helper + blank-identifier suppression * ci: re-trigger to clear flaky E2E shard 3 * fix(gitlab): address final review round — nil guards, store merge, logs, body validation - CheckMRWatch, UpdateReviewWatch, UpdateIssueWatch: nil guards for store/req - handlers: wsNewDiscussionNote rejects empty body - action_presets: Update reads raw stored row (no default freezing); Reset validates workspace_id - service_search.GetStats: log Warn per sub-call failure before zero - event_handlers_gitlab: log Release* dedup-row errors with context - web/store.ts: post-slice merge restores every GitLab sub-state field; extract buildStateOverrides helper - web/default-state.ts: add GitLab fields to defaultState + mergeInitialState via mergeGitLabFields helper - web/gitlab-api.ts: UpdateXxxWatchRequest now Omit workspace_id Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
386 lines
12 KiB
TypeScript
386 lines
12 KiB
TypeScript
import { fetchJson, type ApiRequestOptions } from "../client";
|
|
import type {
|
|
GitLabStatus,
|
|
GitLabConfigureTokenResponse,
|
|
GitLabClearTokenResponse,
|
|
GitLabConfigureHostResponse,
|
|
TaskMR,
|
|
TaskMRsResponse,
|
|
MR,
|
|
Issue,
|
|
MRSearchPage,
|
|
IssueSearchPage,
|
|
} from "@/lib/types/gitlab";
|
|
|
|
export async function fetchGitLabStatus(options?: ApiRequestOptions) {
|
|
return fetchJson<GitLabStatus>("/api/v1/gitlab/status", options);
|
|
}
|
|
|
|
export async function configureGitLabToken(token: string) {
|
|
return fetchJson<GitLabConfigureTokenResponse>("/api/v1/gitlab/token", {
|
|
init: { method: "POST", body: JSON.stringify({ token }) },
|
|
});
|
|
}
|
|
|
|
export async function clearGitLabToken() {
|
|
return fetchJson<GitLabClearTokenResponse>("/api/v1/gitlab/token", {
|
|
init: { method: "DELETE" },
|
|
});
|
|
}
|
|
|
|
export async function configureGitLabHost(host: string) {
|
|
return fetchJson<GitLabConfigureHostResponse>("/api/v1/gitlab/host", {
|
|
init: { method: "POST", body: JSON.stringify({ host }) },
|
|
});
|
|
}
|
|
|
|
/** List every MR association for tasks in a workspace, grouped by task ID. */
|
|
export async function listWorkspaceTaskMRs(workspaceId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<TaskMRsResponse>(
|
|
`/api/v1/gitlab/workspaces/${encodeURIComponent(workspaceId)}/task-mrs`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
/** List the MRs linked to a single task. */
|
|
export async function listTaskMRs(taskId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ task_mrs: TaskMR[] | null }>(
|
|
`/api/v1/gitlab/tasks/${encodeURIComponent(taskId)}/mrs`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sync a task↔MR row from GitLab. Used by the `pr` skill after creating an MR
|
|
* and by the topbar's manual refresh. project_path is "namespace/path".
|
|
*/
|
|
export async function syncTaskMR(
|
|
taskId: string,
|
|
body: { project_path: string; iid: number; repository_id?: string },
|
|
) {
|
|
return fetchJson<TaskMR>(`/api/v1/gitlab/tasks/${encodeURIComponent(taskId)}/mrs/sync`, {
|
|
init: { method: "POST", body: JSON.stringify(body) },
|
|
});
|
|
}
|
|
|
|
/** Search the current user's MRs. filter is one of "assigned", "authored",
|
|
* "review_requested" (matches GitLab's `scope` query param). */
|
|
export async function searchUserMRs(params: {
|
|
filter?: string;
|
|
customQuery?: string;
|
|
page?: number;
|
|
perPage?: number;
|
|
}) {
|
|
const qs = new URLSearchParams();
|
|
if (params.filter) qs.set("filter", params.filter);
|
|
if (params.customQuery) qs.set("custom_query", params.customQuery);
|
|
if (params.page) qs.set("page", String(params.page));
|
|
if (params.perPage) qs.set("per_page", String(params.perPage));
|
|
return fetchJson<MRSearchPage>(`/api/v1/gitlab/user/mrs?${qs.toString()}`, {
|
|
cache: "no-store",
|
|
});
|
|
}
|
|
|
|
/** Search the current user's issues. */
|
|
export async function searchUserIssues(params: {
|
|
filter?: string;
|
|
customQuery?: string;
|
|
page?: number;
|
|
perPage?: number;
|
|
}) {
|
|
const qs = new URLSearchParams();
|
|
if (params.filter) qs.set("filter", params.filter);
|
|
if (params.customQuery) qs.set("custom_query", params.customQuery);
|
|
if (params.page) qs.set("page", String(params.page));
|
|
if (params.perPage) qs.set("per_page", String(params.perPage));
|
|
return fetchJson<IssueSearchPage>(`/api/v1/gitlab/user/issues?${qs.toString()}`, {
|
|
cache: "no-store",
|
|
});
|
|
}
|
|
|
|
export type { MR, Issue, MRSearchPage, IssueSearchPage };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Watches / presets / write actions (parity with GitHub)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import type {
|
|
ReviewWatch,
|
|
IssueWatch,
|
|
MRWatch,
|
|
GitLabStats,
|
|
GitLabActionPresets,
|
|
GitLabProject,
|
|
ProjectMergeMethods,
|
|
GitLabMRFeedback,
|
|
GitLabMRFile,
|
|
GitLabMRCommit,
|
|
GitLabRepoBranch,
|
|
} from "@/lib/types/gitlab";
|
|
|
|
// --- Watches ---
|
|
|
|
export async function listMRWatches(
|
|
filters?: { sessionId?: string; taskId?: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
const qs = new URLSearchParams();
|
|
if (filters?.sessionId) qs.set("session_id", filters.sessionId);
|
|
if (filters?.taskId) qs.set("task_id", filters.taskId);
|
|
return fetchJson<{ watches: MRWatch[] }>(
|
|
`/api/v1/gitlab/watches/mr${qs.size > 0 ? `?${qs.toString()}` : ""}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function deleteMRWatch(id: string) {
|
|
return fetchJson<{ deleted: boolean }>(`/api/v1/gitlab/watches/mr/${encodeURIComponent(id)}`, {
|
|
init: { method: "DELETE" },
|
|
});
|
|
}
|
|
|
|
export type CreateReviewWatchRequest = Omit<
|
|
ReviewWatch,
|
|
"id" | "enabled" | "last_polled_at" | "created_at" | "updated_at"
|
|
>;
|
|
// workspace_id is fixed at creation; the backend update schema ignores it.
|
|
// Excluding it here keeps the type honest so callers don't think they can
|
|
// move a watch between workspaces by sending a different id.
|
|
export type UpdateReviewWatchRequest = Partial<Omit<CreateReviewWatchRequest, "workspace_id">> & {
|
|
enabled?: boolean;
|
|
};
|
|
|
|
export async function listReviewWatches(workspaceId?: string, options?: ApiRequestOptions) {
|
|
const qs = new URLSearchParams();
|
|
if (workspaceId) qs.set("workspace_id", workspaceId);
|
|
return fetchJson<{ watches: ReviewWatch[] }>(
|
|
`/api/v1/gitlab/watches/review${qs.size > 0 ? `?${qs.toString()}` : ""}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function createReviewWatch(req: CreateReviewWatchRequest) {
|
|
return fetchJson<ReviewWatch>(`/api/v1/gitlab/watches/review`, {
|
|
init: { method: "POST", body: JSON.stringify(req) },
|
|
});
|
|
}
|
|
|
|
export async function updateReviewWatch(id: string, req: UpdateReviewWatchRequest) {
|
|
return fetchJson<ReviewWatch>(`/api/v1/gitlab/watches/review/${encodeURIComponent(id)}`, {
|
|
init: { method: "PUT", body: JSON.stringify(req) },
|
|
});
|
|
}
|
|
|
|
export async function deleteReviewWatch(id: string) {
|
|
return fetchJson<{ deleted: boolean }>(
|
|
`/api/v1/gitlab/watches/review/${encodeURIComponent(id)}`,
|
|
{ init: { method: "DELETE" } },
|
|
);
|
|
}
|
|
|
|
export async function triggerReviewWatch(id: string) {
|
|
return fetchJson<{ mrs: MR[]; count: number }>(
|
|
`/api/v1/gitlab/watches/review/${encodeURIComponent(id)}/trigger`,
|
|
{ init: { method: "POST" } },
|
|
);
|
|
}
|
|
|
|
export async function triggerAllReviewWatches() {
|
|
return fetchJson<{ count: number }>(`/api/v1/gitlab/watches/review/trigger-all`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
export type CreateIssueWatchRequest = Omit<
|
|
IssueWatch,
|
|
"id" | "enabled" | "last_polled_at" | "created_at" | "updated_at"
|
|
>;
|
|
// See UpdateReviewWatchRequest for the workspace_id rationale.
|
|
export type UpdateIssueWatchRequest = Partial<Omit<CreateIssueWatchRequest, "workspace_id">> & {
|
|
enabled?: boolean;
|
|
};
|
|
|
|
export async function listIssueWatches(workspaceId?: string, options?: ApiRequestOptions) {
|
|
const qs = new URLSearchParams();
|
|
if (workspaceId) qs.set("workspace_id", workspaceId);
|
|
return fetchJson<{ watches: IssueWatch[] }>(
|
|
`/api/v1/gitlab/watches/issue${qs.size > 0 ? `?${qs.toString()}` : ""}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function createIssueWatch(req: CreateIssueWatchRequest) {
|
|
return fetchJson<IssueWatch>(`/api/v1/gitlab/watches/issue`, {
|
|
init: { method: "POST", body: JSON.stringify(req) },
|
|
});
|
|
}
|
|
|
|
export async function updateIssueWatch(id: string, req: UpdateIssueWatchRequest) {
|
|
return fetchJson<IssueWatch>(`/api/v1/gitlab/watches/issue/${encodeURIComponent(id)}`, {
|
|
init: { method: "PUT", body: JSON.stringify(req) },
|
|
});
|
|
}
|
|
|
|
export async function deleteIssueWatch(id: string) {
|
|
return fetchJson<{ deleted: boolean }>(`/api/v1/gitlab/watches/issue/${encodeURIComponent(id)}`, {
|
|
init: { method: "DELETE" },
|
|
});
|
|
}
|
|
|
|
export async function triggerIssueWatch(id: string) {
|
|
return fetchJson<{ issues: Issue[]; count: number }>(
|
|
`/api/v1/gitlab/watches/issue/${encodeURIComponent(id)}/trigger`,
|
|
{ init: { method: "POST" } },
|
|
);
|
|
}
|
|
|
|
export async function triggerAllIssueWatches() {
|
|
return fetchJson<{ count: number }>(`/api/v1/gitlab/watches/issue/trigger-all`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
// --- Cleanup ---
|
|
|
|
export async function cleanupReviewTasks() {
|
|
return fetchJson<{ deleted: number }>(`/api/v1/gitlab/cleanup/review-tasks`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
export async function cleanupIssueTasks() {
|
|
return fetchJson<{ deleted: number }>(`/api/v1/gitlab/cleanup/issue-tasks`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
// --- Projects ---
|
|
|
|
export async function listUserProjects(options?: ApiRequestOptions) {
|
|
return fetchJson<{ projects: GitLabProject[] }>(`/api/v1/gitlab/projects`, options);
|
|
}
|
|
|
|
export async function searchProjects(query: string) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("query", query);
|
|
return fetchJson<{ projects: GitLabProject[] }>(
|
|
`/api/v1/gitlab/projects/search?${qs.toString()}`,
|
|
);
|
|
}
|
|
|
|
export async function listProjectBranches(project: string) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("project", project);
|
|
return fetchJson<{ branches: GitLabRepoBranch[] }>(
|
|
`/api/v1/gitlab/projects/branches?${qs.toString()}`,
|
|
);
|
|
}
|
|
|
|
export async function getProjectMergeMethods(project: string) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("project", project);
|
|
return fetchJson<ProjectMergeMethods>(`/api/v1/gitlab/projects/merge-methods?${qs.toString()}`);
|
|
}
|
|
|
|
// --- MR write actions ---
|
|
|
|
export async function mergeMR(
|
|
project: string,
|
|
iid: number,
|
|
method?: string,
|
|
squashCommitMessage?: string,
|
|
) {
|
|
return fetchJson<MR>(`/api/v1/gitlab/mrs/merge`, {
|
|
init: {
|
|
method: "PUT",
|
|
body: JSON.stringify({
|
|
project,
|
|
iid,
|
|
method,
|
|
squash_commit_message: squashCommitMessage,
|
|
}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function approveMR(project: string, iid: number) {
|
|
return fetchJson<{ approved: boolean }>(`/api/v1/gitlab/mrs/approve`, {
|
|
init: { method: "POST", body: JSON.stringify({ project, iid }) },
|
|
});
|
|
}
|
|
|
|
export async function unapproveMR(project: string, iid: number) {
|
|
return fetchJson<{ unapproved: boolean }>(`/api/v1/gitlab/mrs/unapprove`, {
|
|
init: { method: "POST", body: JSON.stringify({ project, iid }) },
|
|
});
|
|
}
|
|
|
|
export async function setMRLabels(project: string, iid: number, labels: string[]) {
|
|
return fetchJson<{ updated: boolean }>(`/api/v1/gitlab/mrs/labels`, {
|
|
init: { method: "PUT", body: JSON.stringify({ project, iid, labels }) },
|
|
});
|
|
}
|
|
|
|
export async function setMRAssignees(project: string, iid: number, assigneeIDs: number[]) {
|
|
return fetchJson<{ updated: boolean }>(`/api/v1/gitlab/mrs/assignees`, {
|
|
init: { method: "PUT", body: JSON.stringify({ project, iid, assignee_ids: assigneeIDs }) },
|
|
});
|
|
}
|
|
|
|
export async function getMRFiles(project: string, iid: number) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("project", project);
|
|
qs.set("iid", String(iid));
|
|
return fetchJson<{ files: GitLabMRFile[] }>(`/api/v1/gitlab/mrs/files?${qs.toString()}`);
|
|
}
|
|
|
|
export async function getMRCommits(project: string, iid: number) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("project", project);
|
|
qs.set("iid", String(iid));
|
|
return fetchJson<{ commits: GitLabMRCommit[] }>(`/api/v1/gitlab/mrs/commits?${qs.toString()}`);
|
|
}
|
|
|
|
export async function getMRFeedback(project: string, iid: number) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("project", project);
|
|
qs.set("iid", String(iid));
|
|
return fetchJson<GitLabMRFeedback>(`/api/v1/gitlab/mrs/feedback?${qs.toString()}`, {
|
|
cache: "no-store",
|
|
});
|
|
}
|
|
|
|
// --- Action presets ---
|
|
|
|
export async function getActionPresets(workspaceId: string) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("workspace_id", workspaceId);
|
|
return fetchJson<GitLabActionPresets>(`/api/v1/gitlab/action-presets?${qs.toString()}`);
|
|
}
|
|
|
|
export async function updateActionPresets(
|
|
workspaceId: string,
|
|
body: { mr?: GitLabActionPresets["mr"]; issue?: GitLabActionPresets["issue"] },
|
|
) {
|
|
return fetchJson<GitLabActionPresets>(`/api/v1/gitlab/action-presets`, {
|
|
init: {
|
|
method: "PUT",
|
|
body: JSON.stringify({ workspace_id: workspaceId, ...body }),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function resetActionPresets(workspaceId: string) {
|
|
const qs = new URLSearchParams();
|
|
qs.set("workspace_id", workspaceId);
|
|
return fetchJson<GitLabActionPresets>(`/api/v1/gitlab/action-presets/reset?${qs.toString()}`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
// --- Stats ---
|
|
|
|
export async function fetchGitLabStats() {
|
|
return fetchJson<GitLabStats>(`/api/v1/gitlab/stats`, { cache: "no-store" });
|
|
}
|