mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
* test(repro): demonstrate #7732 GitLab pipeline job details never load in Checks panel Co-authored-by: Orca <help@stably.ai> * fix(gitlab): load pipeline job traces in the Checks side panel (#7732) Expanding a GitLab pipeline job in the Checks panel always showed "No inline details are available for this check.": the mapper dropped the numeric job id, `PRCheckDetail` had nowhere to carry it, and every consumer called the GitHub check-runs API, which returns null for a GitLab job. - carry `gitlabJobId` on `PRCheckDetail` and add the `gitlab-job:` branch to all three identity ladders (panel rows, editor tabs, fix-prompt keys) so same-stage jobs with no web_url stop colliding - add a runtime-routed trace client so SSH/remote workspaces work, not just local IPC, and thread the MR's `projectRef` for fork pipelines - bound the trace in main via the existing `sliceCheckLogTail` (now shared, not GitHub-only) so a multi-megabyte CI log never crosses the 1 MB transport frame cap; strip ANSI/section markers up to the CR only, which keeps each section's visible header and command echo - render the excerpt inline instead of "Log tail available in full details." - feed GitLab traces to "Fix with AI", which previously sent bare check names - skip the fetch for jobs that cannot have a trace (created/manual/skipped) so GitLab's 404 does not replace the benign empty state, and re-arm a failed load when the job's state changes since the panel has no retry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): treat a missing job log as an empty log, not an error (#7732) Round-1 review follow-up. - a job canceled before it started (or whose log was erased/expired) is `completed`/`cancelled`, so the panel fetched its trace, GitLab answered 404, and `classifyGlabError`'s issue-edit copy ("Issue not found — it may have been deleted.") landed verbatim on the auto-expanded check row; main now maps that 404 to an empty trace so the row keeps its benign empty state - keep a missing project a real error (GitLab masks unauthorized projects as 404) and add `classifyJobLogError` so 403/unknown failures stop borrowing issue-edit wording on a job-log read - broaden the empty-log copy in all five catalogs: it now covers erased and expired logs, not only jobs that never ran - e2e: derive the repro screenshot dir from `process.cwd()` (or an env override) instead of a hardcoded POSIX path to a throwaway worktree - bound the raw trace before the ANSI/section passes so a multi-megabyte log is not scanned in full on the main-process event loop - drop the redundant `if (repo)` in `handleFixChecksWithAI` and the now-dead "Log tail available in full details." catalog entry Co-authored-by: Orca <help@stably.ai> * fix(gitlab): address review — project ref on reload, retry re-arm, IPC timeout - Carry the MR's GitLab project ref on the check-details tab so reloading a fork/cross-project job tab fetches the trace from the pipeline's own project. - Re-arm the sidebar retry when a details load resolves to null, not only when it throws; a detail-less row otherwise never retried after the job moved on. - Bound the local `gl.jobTrace` IPC call with the same 30s timeout the runtime RPC path uses — glab runs without a subprocess timeout in main. - Document that the trace 404 -> empty-log mapping is deliberately broad. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
604 lines
17 KiB
TypeScript
604 lines
17 KiB
TypeScript
/* eslint-disable max-lines -- Why: parallel to ipc/github.ts — keeping all
|
|
GitLab IPC handlers co-located keeps the repo-path validation pattern
|
|
reviewable as one surface. */
|
|
import { ipcMain } from 'electron'
|
|
import { resolve } from 'node:path'
|
|
import { toGitLabJobLogExcerptResult } from '../../shared/gitlab-job-log-excerpt'
|
|
import type {
|
|
GitLabIssueUpdate,
|
|
GitLabMRInlineCommentInput,
|
|
GitLabMRUpdate,
|
|
GitLabWorkItem,
|
|
Repo
|
|
} from '../../shared/types'
|
|
import { getRepoExecutionHostId } from '../../shared/execution-host'
|
|
import type { TaskSourceContext } from '../../shared/task-source-context'
|
|
import type { Store } from '../persistence'
|
|
import {
|
|
normalizeGitLabIssueAssignee,
|
|
normalizeGitLabIssueListState,
|
|
normalizeGitLabMRListState,
|
|
normalizeGitLabPositiveInteger,
|
|
normalizeGitLabSearchQuery
|
|
} from '../gitlab/gitlab-preload-args'
|
|
import { recordGitLabProjectRecent } from '../gitlab/gitlab-project-recents'
|
|
import {
|
|
addIssueComment,
|
|
addMRInlineComment,
|
|
addMRComment,
|
|
closeMR,
|
|
createIssue,
|
|
diagnoseAuth,
|
|
getAuthenticatedViewer,
|
|
getJobTrace,
|
|
getIssue,
|
|
getMergeRequest,
|
|
getMergeRequestForBranch,
|
|
getProjectSlug,
|
|
getRateLimit,
|
|
getWorkItemByProjectRef,
|
|
listAssignableUsers,
|
|
listIssues,
|
|
listLabels,
|
|
listMergeRequests,
|
|
listTodos,
|
|
listWorkItems,
|
|
mergeMR,
|
|
reopenMR,
|
|
resolveMRDiscussion,
|
|
retryJob,
|
|
updateIssue,
|
|
updateMR,
|
|
updateMRReviewers
|
|
} from '../gitlab/client'
|
|
import { getWorkItemDetails } from '../gitlab/work-item-details'
|
|
import type { ProjectRef } from '../gitlab/gl-utils'
|
|
import type { LocalGitExecOptions } from '../gitlab/gitlab-project-ref-resolution'
|
|
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
|
|
import type { HostedReviewExecutionOptions } from '../source-control/hosted-review-git-options'
|
|
|
|
type GitLabRepoSelectorArgs = {
|
|
repoPath: string
|
|
repoId?: string | null
|
|
sourceContext?: TaskSourceContext | null
|
|
}
|
|
|
|
function findRegisteredGitLabRepo(args: GitLabRepoSelectorArgs, store: Store): Repo | undefined {
|
|
const sourceRepoId =
|
|
args.sourceContext?.provider === 'gitlab' ? args.sourceContext.repoId?.trim() : null
|
|
const repoId = args.repoId?.trim() || sourceRepoId || null
|
|
if (repoId) {
|
|
const repo = store.getRepo(repoId)
|
|
if (repo) {
|
|
return repo
|
|
}
|
|
}
|
|
const resolvedRepoPath = resolve(args.repoPath)
|
|
return store.getRepos().find((r) => resolve(r.path) === resolvedRepoPath)
|
|
}
|
|
|
|
// Why: mirror github.ts assertRegisteredRepo — main-process handlers
|
|
// must never operate on a path the user hasn't explicitly registered as
|
|
// a repo (filesystem-auth boundary). Source context adds a host check so a
|
|
// task fetched from one machine cannot mutate a same-path repo on another.
|
|
function assertRegisteredRepo(args: GitLabRepoSelectorArgs, store: Store): Repo {
|
|
const repo = findRegisteredGitLabRepo(args, store)
|
|
if (!repo) {
|
|
throw new Error('Access denied: unknown repository path')
|
|
}
|
|
if (
|
|
args.sourceContext?.provider === 'gitlab' &&
|
|
args.sourceContext.hostId !== getRepoExecutionHostId(repo)
|
|
) {
|
|
throw new Error('Access denied: GitLab source host does not match repository host')
|
|
}
|
|
return repo
|
|
}
|
|
|
|
function repoConnectionId(repo: Repo): string | null {
|
|
return repo.connectionId ?? null
|
|
}
|
|
|
|
function localGitOptionArgs(store: Store, repo: Repo): [] | [LocalGitExecOptions] {
|
|
const localGitOptions = getLocalProjectWorktreeGitOptions(store, repo)
|
|
return localGitOptions.wslDistro ? [{ wslDistro: localGitOptions.wslDistro }] : []
|
|
}
|
|
|
|
function hostedReviewOptionArgs(store: Store, repo: Repo): [] | [HostedReviewExecutionOptions] {
|
|
const localGitOptions = getLocalProjectWorktreeGitOptions(store, repo)
|
|
return localGitOptions.wslDistro
|
|
? [{ localGitExecOptions: { wslDistro: localGitOptions.wslDistro } }]
|
|
: []
|
|
}
|
|
|
|
export function registerGitLabHandlers(store: Store): void {
|
|
ipcMain.handle('gitlab:viewer', async () => {
|
|
return getAuthenticatedViewer()
|
|
})
|
|
|
|
ipcMain.handle('gitlab:diagnoseAuth', async () => diagnoseAuth())
|
|
|
|
ipcMain.handle(
|
|
'gitlab:rateLimit',
|
|
async (_event, args?: { force?: boolean; host?: string | null }) =>
|
|
getRateLimit({ force: Boolean(args?.force), host: args?.host ?? null })
|
|
)
|
|
|
|
ipcMain.handle('gitlab:projectSlug', async (_event, args: GitLabRepoSelectorArgs) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return getProjectSlug(repo.path, repoConnectionId(repo), ...hostedReviewOptionArgs(store, repo))
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gitlab:mrForBranch',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & { branch: string; linkedMRIid?: number | null }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return getMergeRequestForBranch(
|
|
repo.path,
|
|
args.branch,
|
|
args.linkedMRIid ?? null,
|
|
repoConnectionId(repo),
|
|
...hostedReviewOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gitlab:mr', async (_event, args: GitLabRepoSelectorArgs & { iid: number }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return getMergeRequest(
|
|
repo.path,
|
|
args.iid,
|
|
repoConnectionId(repo),
|
|
...hostedReviewOptionArgs(store, repo)
|
|
)
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gitlab:listMRs',
|
|
async (
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
repoId?: string | null
|
|
sourceContext?: TaskSourceContext | null
|
|
state?: 'opened' | 'merged' | 'closed' | 'all'
|
|
page?: number
|
|
perPage?: number
|
|
query?: string
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
const state = normalizeGitLabMRListState(args.state)
|
|
const page = normalizeGitLabPositiveInteger(args.page, 1, 10_000)
|
|
const perPage = normalizeGitLabPositiveInteger(args.perPage, 20, 100)
|
|
return listMergeRequests(
|
|
repo.path,
|
|
state,
|
|
page,
|
|
perPage,
|
|
repo.issueSourcePreference,
|
|
normalizeGitLabSearchQuery(args.query),
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:issue',
|
|
async (_event, args: GitLabRepoSelectorArgs & { number: number }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return getIssue(
|
|
repo.path,
|
|
args.number,
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:listIssues',
|
|
async (
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
repoId?: string | null
|
|
sourceContext?: TaskSourceContext | null
|
|
state?: 'opened' | 'closed' | 'all'
|
|
assignee?: string
|
|
limit?: number
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
const limit = normalizeGitLabPositiveInteger(args.limit, 20, 100)
|
|
const state = normalizeGitLabIssueListState(args.state)
|
|
const assignee = normalizeGitLabIssueAssignee(args.assignee)
|
|
const result = await listIssues(
|
|
repo.path,
|
|
limit,
|
|
repo.issueSourcePreference,
|
|
state,
|
|
assignee,
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
// Why: Tasks page expects GitLabWorkItem[] so it can share row
|
|
// rendering with MRs. Map IssueInfo → WorkItem here so the renderer
|
|
// doesn't need a separate code path.
|
|
const workItems: GitLabWorkItem[] = result.items.map((issue) => ({
|
|
id: `gitlab-issue-${repo.id}-${issue.number}`,
|
|
type: 'issue' as const,
|
|
number: issue.number,
|
|
title: issue.title,
|
|
state: issue.state,
|
|
url: issue.url,
|
|
labels: issue.labels,
|
|
updatedAt: issue.updatedAt ?? '',
|
|
author: issue.author ?? null,
|
|
repoId: repo.id
|
|
}))
|
|
return { items: workItems, ...(result.error ? { error: result.error } : {}) }
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:createIssue',
|
|
async (_event, args: GitLabRepoSelectorArgs & { title: string; body: string }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return createIssue(
|
|
repo.path,
|
|
args.title,
|
|
args.body,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:updateIssue',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & { number: number; updates: GitLabIssueUpdate }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return updateIssue(
|
|
repo.path,
|
|
args.number,
|
|
args.updates,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:addIssueComment',
|
|
async (_event, args: GitLabRepoSelectorArgs & { number: number; body: string }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return addIssueComment(
|
|
repo.path,
|
|
args.number,
|
|
args.body,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gitlab:listLabels', async (_event, args: GitLabRepoSelectorArgs) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return listLabels(
|
|
repo.path,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
})
|
|
|
|
ipcMain.handle('gitlab:listAssignableUsers', async (_event, args: GitLabRepoSelectorArgs) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return listAssignableUsers(
|
|
repo.path,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
})
|
|
|
|
// Why: combined MR + issue list — Tasks screen and any future picker
|
|
// that wants a unified view. Centralizes the merge / sort logic so
|
|
// callers don't have to re-implement it.
|
|
ipcMain.handle(
|
|
'gitlab:listWorkItems',
|
|
async (
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
repoId?: string | null
|
|
sourceContext?: TaskSourceContext | null
|
|
state?: 'opened' | 'merged' | 'closed' | 'all'
|
|
page?: number
|
|
perPage?: number
|
|
query?: string
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return listWorkItems(
|
|
repo.path,
|
|
normalizeGitLabMRListState(args.state),
|
|
normalizeGitLabPositiveInteger(args.page, 1, 10_000),
|
|
normalizeGitLabPositiveInteger(args.perPage, 20, 100),
|
|
repo.issueSourcePreference,
|
|
normalizeGitLabSearchQuery(args.query),
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
// Why: aggregated dialog payload — body + discussions + pipeline jobs.
|
|
// Powers GitLabItemDialog's tabs.
|
|
ipcMain.handle(
|
|
'gitlab:workItemDetails',
|
|
async (_event, args: GitLabRepoSelectorArgs & { iid: number; type: 'issue' | 'mr' }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return getWorkItemDetails(
|
|
repo.path,
|
|
args.iid,
|
|
args.type,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:closeMR',
|
|
async (_event, args: GitLabRepoSelectorArgs & { iid: number }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return closeMR(
|
|
repo.path,
|
|
args.iid,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:reopenMR',
|
|
async (_event, args: GitLabRepoSelectorArgs & { iid: number }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return reopenMR(
|
|
repo.path,
|
|
args.iid,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:mergeMR',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & { iid: number; method?: 'merge' | 'squash' | 'rebase' }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return mergeMR(
|
|
repo.path,
|
|
args.iid,
|
|
args.method ?? 'merge',
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:updateMR',
|
|
async (_event, args: GitLabRepoSelectorArgs & { iid: number; updates: GitLabMRUpdate }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return updateMR(
|
|
repo.path,
|
|
args.iid,
|
|
args.updates,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:updateMRReviewers',
|
|
async (
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
repoId?: string | null
|
|
sourceContext?: TaskSourceContext | null
|
|
iid: number
|
|
reviewerIds: number[]
|
|
projectRef?: ProjectRef | null
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return updateMRReviewers(
|
|
repo.path,
|
|
args.iid,
|
|
args.reviewerIds,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
args.projectRef,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:addMRComment',
|
|
async (_event, args: GitLabRepoSelectorArgs & { iid: number; body: string }) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return addMRComment(
|
|
repo.path,
|
|
args.iid,
|
|
args.body,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:addMRInlineComment',
|
|
async (
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
repoId?: string | null
|
|
sourceContext?: TaskSourceContext | null
|
|
iid: number
|
|
input: GitLabMRInlineCommentInput
|
|
projectRef?: ProjectRef | null
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return addMRInlineComment(
|
|
repo.path,
|
|
args.iid,
|
|
args.input,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
args.projectRef,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:resolveMRDiscussion',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & { iid: number; discussionId: string; resolved: boolean }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return resolveMRDiscussion(
|
|
repo.path,
|
|
args.iid,
|
|
args.discussionId,
|
|
args.resolved,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
undefined,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:jobTrace',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & {
|
|
jobId: number
|
|
projectRef?: ProjectRef | null
|
|
logExcerpt?: boolean
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
const result = await getJobTrace(
|
|
repo.path,
|
|
args.jobId,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
args.projectRef,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
return args.logExcerpt ? toGitLabJobLogExcerptResult(result) : result
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gitlab:retryJob',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & { jobId: number; projectRef?: ProjectRef | null }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return retryJob(
|
|
repo.path,
|
|
args.jobId,
|
|
repo.issueSourcePreference,
|
|
repoConnectionId(repo),
|
|
args.projectRef,
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
}
|
|
)
|
|
|
|
// Why: My Todos surface — cross-project, user-scoped. The repoPath is
|
|
// only used for the registered-repo guard; `glab api todos` doesn't
|
|
// care about cwd because the endpoint is user-scoped.
|
|
ipcMain.handle('gitlab:todos', async (_event, args: GitLabRepoSelectorArgs) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
return listTodos(repo.path, repoConnectionId(repo), ...localGitOptionArgs(store, repo))
|
|
})
|
|
|
|
// Why: paste-URL flow in the picker. The user pastes a GitLab URL that
|
|
// may target a project different from the local checkout's remote, so
|
|
// the call carries the parsed project path explicitly rather than
|
|
// resolving from cwd.
|
|
ipcMain.handle(
|
|
'gitlab:workItemByPath',
|
|
async (
|
|
_event,
|
|
args: GitLabRepoSelectorArgs & {
|
|
host: string
|
|
path: string
|
|
iid: number
|
|
type: 'issue' | 'mr'
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args, store)
|
|
const projectRef: ProjectRef = { host: args.host, path: args.path }
|
|
const result = await getWorkItemByProjectRef(
|
|
repo.path,
|
|
projectRef,
|
|
args.iid,
|
|
args.type,
|
|
repoConnectionId(repo),
|
|
...localGitOptionArgs(store, repo)
|
|
)
|
|
// Why: only persist a recent entry when the lookup actually
|
|
// produced an item. A 404 / auth failure shouldn't pollute the
|
|
// user's recents list with project paths they can't read.
|
|
if (result) {
|
|
recordGitLabProjectRecent(store, args.host, args.path)
|
|
}
|
|
return result
|
|
}
|
|
)
|
|
}
|