Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion config/build-plugins/plain-node-entry-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type OutputChunk = Rollup.OutputChunk
// The CLI loads these paths after electron-vite replaces out/main.
export const CLI_MAIN_ENTRY_NAMES = [
'agent-hooks/managed-agent-hook-controls',
'gitlab/project-ref-parser',
'orca-profiles/profile-index-store',
'claude-accounts/keychain',
'skills/bundled-skill-install/cli-entry',
Expand Down
1 change: 0 additions & 1 deletion config/electron-builder.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,6 @@ module.exports = {
'out/main/cursor/**',
'out/main/droid/**',
'out/main/gemini/**',
'out/main/gitlab/project-ref-parser.js',
'out/main/grok/**',
'out/main/hermes/**',
'out/main/asar-transparent-fs.js',
Expand Down
1 change: 0 additions & 1 deletion config/scripts/electron-vite-output-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ describe('Electron Vite output contract', () => {
electronBuilderConfig.asarUnpack
).createFilter()
for (const name of [
'gitlab/project-ref-parser',
'macos-tree-clone',
'orca-profiles/profile-index-store',
'persistence/profile-state/profile-state-access',
Expand Down
2 changes: 1 addition & 1 deletion src/cli/handlers/worktree-gitlab-link-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseRemoteProjectRefCandidate } from '../../main/gitlab/project-ref-parser'
import { parseRemoteProjectRefCandidate } from '../../shared/gitlab-project-ref-parser'
import type { Repo } from '../../shared/repo-types'
import type { RuntimeWorktreeRecord } from '../../shared/runtime-types'
import { parseGitLabIssueOrMRLink, type ProjectSlug } from '../../shared/new-workspace/gitlab-links'
Expand Down
2 changes: 1 addition & 1 deletion src/main/gitlab/gitlab-known-host-probe.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { runCoalescedProbe, type CoalescedProbes } from '../git/coalesced-probe'
import { NEGATIVE_ENTRY_TTL_MS } from '../git/remote-ref-probe-cache'
import { glabExecFileAsync } from '../git/runner'
import { DEFAULT_GITLAB_HOSTS, normalizeGitLabHost } from './project-ref-parser'
import { DEFAULT_GITLAB_HOSTS, normalizeGitLabHost } from '../../shared/gitlab-project-ref-parser'
import type { GitAdmissionTier } from '../git/command-runner/git-exec-options'

export type LocalGitExecOptions = {
Expand Down
2 changes: 1 addition & 1 deletion src/main/gitlab/gitlab-project-ref-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
parseGitLabProjectRef,
parseRemoteProjectRefCandidate,
type ProjectRef
} from './project-ref-parser'
} from '../../shared/gitlab-project-ref-parser'

export { DEFAULT_GITLAB_HOSTS, parseGitLabProjectRef }
export type { ProjectRef }
Expand Down
58 changes: 55 additions & 3 deletions src/main/ipc/gitlab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Store } from '../persistence'
import type { Repo } from '../../shared/repo-types'
import { toSshExecutionHostId } from '../../shared/execution-host'
import type * as GitLabUtils from '../gitlab/gl-utils'

const ORIGINAL_PLATFORM = process.platform

Expand Down Expand Up @@ -38,7 +39,8 @@ const {
getWorkItemByProjectRefMock,
getMergeRequestMock,
getMergeRequestForBranchMock,
getProjectSlugMock
getProjectSlugMock,
getGlabKnownHostsMock
} = vi.hoisted(() => ({
ipcHandlers: new Map<string, (...args: unknown[]) => unknown>(),
listMergeRequestsMock: vi.fn(),
Expand All @@ -65,7 +67,8 @@ const {
getWorkItemByProjectRefMock: vi.fn(),
getMergeRequestMock: vi.fn(),
getMergeRequestForBranchMock: vi.fn(),
getProjectSlugMock: vi.fn()
getProjectSlugMock: vi.fn(),
getGlabKnownHostsMock: vi.fn()
}))

vi.mock('electron', () => ({
Expand Down Expand Up @@ -106,6 +109,11 @@ vi.mock('../gitlab/client', () => ({
updateMRReviewers: updateMRReviewersMock
}))

vi.mock('../gitlab/gl-utils', async (importOriginal) => ({
...(await importOriginal<typeof GitLabUtils>()),
getGlabKnownHosts: getGlabKnownHostsMock
}))

vi.mock('../gitlab/work-item-details', () => ({
getWorkItemDetails: getWorkItemDetailsMock
}))
Expand Down Expand Up @@ -171,7 +179,8 @@ describe('GitLab IPC handlers', () => {
getWorkItemByProjectRefMock,
getMergeRequestMock,
getMergeRequestForBranchMock,
getProjectSlugMock
getProjectSlugMock,
getGlabKnownHostsMock
]) {
mock.mockReset()
}
Expand Down Expand Up @@ -274,6 +283,46 @@ describe('GitLab IPC handlers', () => {
)
})

it("answers glab's known hosts from the repo's own execution context", async () => {
const remoteRepo = repo({
id: 'repo-ssh',
path: '/ssh/orca',
connectionId: 'builder',
executionHostId: toSshExecutionHostId('builder')
})
getGlabKnownHostsMock.mockResolvedValueOnce(['gitlab.com', 'git.example.co.jp'])
registerGitLabHandlers(storeWithRepos([repo(), remoteRepo]) as Store)

const handler = ipcHandlers.get('gitlab:knownHosts')
await expect(handler?.(null, { repoPath: '/ssh/orca', repoId: 'repo-ssh' })).resolves.toEqual([
'gitlab.com',
'git.example.co.jp'
])
expect(getGlabKnownHostsMock).toHaveBeenCalledWith('builder')

await expect(handler?.(null, { repoPath: '/not/registered' })).rejects.toThrow(
'Access denied: unknown repository path'
)
expect(getGlabKnownHostsMock).toHaveBeenCalledTimes(1)
})

it('asks the SSH host of a row that names it only as its execution host', async () => {
const remoteRepo = repo({
id: 'repo-ssh',
path: '/ssh/orca',
executionHostId: toSshExecutionHostId('builder')
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The knownHosts handler reads only the store methods stubbed here.
registerGitLabHandlers(storeWithRepos([remoteRepo]) as Store)

await ipcHandlers.get('gitlab:knownHosts')?.(null, {
repoPath: '/ssh/orca',
repoId: 'repo-ssh'
})

expect(getGlabKnownHostsMock).toHaveBeenCalledWith('builder')
})

it('rejects source context for a different host', async () => {
registerGitLabHandlers(
storeWithRepos([repo({ id: 'repo-local', path: '/local/orca' })]) as Store
Expand Down Expand Up @@ -360,6 +409,7 @@ describe('GitLab IPC handlers', () => {
listLabelsMock.mockResolvedValue([])
listAssignableUsersMock.mockResolvedValue([])
listTodosMock.mockResolvedValue([])
getGlabKnownHostsMock.mockResolvedValue(['gitlab.com'])
getProjectSlugMock.mockResolvedValue({ host: 'gitlab.com', path: 'stablyai/orca' })
getMergeRequestForBranchMock.mockResolvedValue(null)
getMergeRequestMock.mockResolvedValue(null)
Expand Down Expand Up @@ -409,6 +459,7 @@ describe('GitLab IPC handlers', () => {
await ipcHandlers.get('gitlab:listLabels')?.(null, { repoPath: '/local/orca' })
await ipcHandlers.get('gitlab:listAssignableUsers')?.(null, { repoPath: '/local/orca' })
await ipcHandlers.get('gitlab:todos')?.(null, { repoPath: '/local/orca' })
await ipcHandlers.get('gitlab:knownHosts')?.(null, { repoPath: '/local/orca' })

const hostedReviewOptions = { localGitExecOptions: localGitOptions }
expect(getProjectSlugMock).toHaveBeenCalledWith('/local/orca', null, hostedReviewOptions)
Expand Down Expand Up @@ -486,6 +537,7 @@ describe('GitLab IPC handlers', () => {
localGitOptions
)
expect(listTodosMock).toHaveBeenCalledWith('/local/orca', null, localGitOptions)
expect(getGlabKnownHostsMock).toHaveBeenCalledWith(null, localGitOptions)
})

it('routes local WSL project GitLab MR details, review, job, and pasted URL IPC through project git options', async () => {
Expand Down
9 changes: 9 additions & 0 deletions src/main/ipc/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getRateLimit,
listTodos
} from '../gitlab/client'
import { getGlabKnownHosts } from '../gitlab/gl-utils'
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
import { registerGitLabCiJobHandlers } from './gitlab-ci-job-handlers'
import { registerGitLabIssueHandlers } from './gitlab-issue-handlers'
import { registerGitLabMergeRequestMutationHandlers } from './gitlab-merge-request-mutation-handlers'
Expand All @@ -33,6 +35,13 @@ export function registerGitLabHandlers(store: Store): void {
getRateLimit({ force: Boolean(args?.force), host: args?.host ?? null })
)

// Why: answers from the repo's own execution context so the Tasks page can tell a
// self-hosted GitLab origin from a GitHub one without querying either forge.
ipcMain.handle('gitlab:knownHosts', async (_event, args: GitLabRepoSelectorArgs) => {
const repo = assertRegisteredRepo(args, store)
return getGlabKnownHosts(getRepoRoutingConnectionId(repo), ...localGitOptionArgs(store, repo))
})

ipcMain.handle('gitlab:projectSlug', async (_event, args: GitLabRepoSelectorArgs) => {
const repo = assertRegisteredRepo(args, store)
return getProjectSlug(repo.path, repoConnectionId(repo), ...hostedReviewOptionArgs(store, repo))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getGitLabJobTraceMock,
getGitLabWorkItemByProjectRefMock,
getGitLabWorkItemDetailsMock,
getGlabKnownHostsMock,
listGitLabIssuesMock,
listGitLabLabelsMock,
listGitLabAssignableUsersMock,
Expand Down Expand Up @@ -90,6 +91,7 @@ describe('OrcaRuntimeService', () => {
await runtime.listGitLabRepoWorkItems(TEST_REPO_ID, 'closed', 2, 25, 'ambiguous selector')
const issues = await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'opened', '@me', 50, 3)
await runtime.listGitLabRepoTodos(TEST_REPO_ID)
await runtime.listGitLabRepoKnownHosts(TEST_REPO_ID)
await runtime.listGitLabRepoLabels(TEST_REPO_ID)
await runtime.listGitLabRepoAssignableUsers(TEST_REPO_ID)
await runtime.createGitLabRepoIssue(TEST_REPO_ID, 'New issue', 'Body')
Expand Down Expand Up @@ -120,6 +122,7 @@ describe('OrcaRuntimeService', () => {
'issue'
)

expect(getGlabKnownHostsMock).toHaveBeenCalledWith('ssh-1')
expect(listGitLabMergeRequestsMock).toHaveBeenCalledWith(
'/remote/repo',
'closed',
Expand Down Expand Up @@ -315,6 +318,7 @@ describe('OrcaRuntimeService', () => {
await runtime.listGitLabRepoWorkItems(TEST_REPO_ID, 'opened', 1, 20)
await runtime.listGitLabRepoIssues(TEST_REPO_ID, 'opened', undefined, 20)
await runtime.listGitLabRepoTodos(TEST_REPO_ID)
await runtime.listGitLabRepoKnownHosts(TEST_REPO_ID)
await runtime.listGitLabRepoLabels(TEST_REPO_ID)
await runtime.createGitLabRepoIssue(TEST_REPO_ID, 'Title', 'Body')
await runtime.updateGitLabRepoIssue(TEST_REPO_ID, 7, { body: 'Updated' })
Expand Down Expand Up @@ -351,6 +355,7 @@ describe('OrcaRuntimeService', () => {
1
)
expect(listGitLabTodosMock).toHaveBeenCalledWith(TEST_REPO_PATH, null, localGitOptions)
expect(getGlabKnownHostsMock).toHaveBeenCalledWith(null, localGitOptions)
expect(listGitLabLabelsMock).toHaveBeenCalledWith(
TEST_REPO_PATH,
undefined,
Expand Down
4 changes: 4 additions & 0 deletions src/main/runtime/rpc/methods/gitlab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('gitlab RPC methods', () => {
})

it('routes GitLab task queries and mutations to the runtime server', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The dispatcher calls only the runtime methods stubbed here.
const runtime = {
getRuntimeId: () => 'test-runtime',
diagnoseGitLabAuth: vi.fn().mockResolvedValue({ glabAvailable: true }),
Expand All @@ -40,6 +41,7 @@ describe('gitlab RPC methods', () => {
listGitLabRepoWorkItems: vi.fn().mockResolvedValue({ items: [] }),
listGitLabRepoIssues: vi.fn().mockResolvedValue({ items: [] }),
listGitLabRepoTodos: vi.fn().mockResolvedValue([{ id: 1 }]),
listGitLabRepoKnownHosts: vi.fn().mockResolvedValue(['gitlab.com']),
listGitLabRepoLabels: vi.fn().mockResolvedValue(['bug']),
createGitLabRepoIssue: vi.fn().mockResolvedValue({ ok: true, number: 7 }),
updateGitLabRepoIssue: vi.fn().mockResolvedValue({ ok: true }),
Expand Down Expand Up @@ -98,6 +100,7 @@ describe('gitlab RPC methods', () => {
})
)
await dispatcher.dispatch(makeRequest('gitlab.todos', { repo: 'id:repo-1' }))
await dispatcher.dispatch(makeRequest('gitlab.knownHosts', { repo: 'id:repo-1' }))
await dispatcher.dispatch(makeRequest('gitlab.listLabels', { repo: 'id:repo-1' }))
await dispatcher.dispatch(
makeRequest('gitlab.updateIssue', {
Expand Down Expand Up @@ -228,6 +231,7 @@ describe('gitlab RPC methods', () => {
expect(runtime.listGitLabRepoIssues).toHaveBeenCalledWith('id:repo-1', 'opened', '@me', 50, 2)
expect(runtime.createGitLabRepoIssue).toHaveBeenCalledWith('id:repo-1', 'Fix bug', 'Details')
expect(runtime.listGitLabRepoTodos).toHaveBeenCalledWith('id:repo-1')
expect(runtime.listGitLabRepoKnownHosts).toHaveBeenCalledWith('id:repo-1')
expect(runtime.listGitLabRepoLabels).toHaveBeenCalledWith('id:repo-1')
expect(runtime.updateGitLabRepoIssue).toHaveBeenCalledWith(
'id:repo-1',
Expand Down
6 changes: 6 additions & 0 deletions src/main/runtime/rpc/methods/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export const GITLAB_METHODS = [
params: RepoSelector,
handler: async (params, { runtime }) => runtime.listGitLabRepoTodos(params.repo)
}),
defineMethod({
name: 'gitlab.knownHosts',
permission: 'workspace',
params: RepoSelector,
handler: async (params, { runtime }) => runtime.listGitLabRepoKnownHosts(params.repo)
}),
defineMethod({
name: 'gitlab.diagnoseAuth',
permission: 'workspace',
Expand Down
6 changes: 6 additions & 0 deletions src/main/runtime/runtime-gitlab-query-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../gitlab/gitlab-preload-args'
import { getWorkItemDetails } from '../gitlab/work-item-details'
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
import { getGlabKnownHosts } from '../gitlab/gl-utils'

type LocalGitArgs = [] | [{ wslDistro?: string }]

Expand Down Expand Up @@ -124,6 +125,11 @@ export class RuntimeGitLabQueryCommands {
)
}

async listGitLabRepoKnownHosts(repoSelector: string): Promise<readonly string[]> {
const repo = await this.deps.resolveRepo(repoSelector)
return getGlabKnownHosts(getRepoRoutingConnectionId(repo), ...this.deps.getLocalGitArgs(repo))
}

diagnoseGitLabAuth(): Promise<Awaited<ReturnType<typeof diagnoseAuth>>> {
return diagnoseAuth()
}
Expand Down
1 change: 1 addition & 0 deletions src/main/runtime/runtime-review-command-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function installRuntimeReviewCommandSurface(
listGitLabRepoMRs: glq.listGitLabRepoMRs.bind(glq),
listGitLabRepoIssues: glq.listGitLabRepoIssues.bind(glq),
listGitLabRepoTodos: glq.listGitLabRepoTodos.bind(glq),
listGitLabRepoKnownHosts: glq.listGitLabRepoKnownHosts.bind(glq),
diagnoseGitLabAuth: glq.diagnoseGitLabAuth.bind(glq),
getGitLabRateLimit: glq.getGitLabRateLimit.bind(glq),
listGitLabRepoLabels: glq.listGitLabRepoLabels.bind(glq),
Expand Down
2 changes: 2 additions & 0 deletions src/preload/api/gitlab-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export type GitLabApi = {
listAssignableUsers: (args: GitLabRepoSelectorArgs) => Promise<GitLabAssignableUser[]>
/** Cross-project user-scoped todos (gitlab.com/dashboard/todos). */
todos: (args: GitLabRepoSelectorArgs) => Promise<GitLabTodo[]>
/** Hosts glab is signed in to (plus gitlab.com) on the machine that runs the repo's glab. */
knownHosts: (args: GitLabRepoSelectorArgs) => Promise<string[]>
workItemDetails: (
args: GitLabRepoSelectorArgs & {
iid: number
Expand Down
3 changes: 3 additions & 0 deletions src/preload/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export const glApi = {

todos: (args: GitLabRepoSelectorArgs) => ipcRenderer.invoke('gitlab:todos', args),

knownHosts: (args: GitLabRepoSelectorArgs): Promise<string[]> =>
ipcRenderer.invoke('gitlab:knownHosts', args),

workItemDetails: (
args: GitLabRepoSelectorArgs & {
iid: number
Expand Down
55 changes: 55 additions & 0 deletions src/renderer/src/components/task-page/SkippedReposNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useState } from 'react'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { translate } from '@/i18n/i18n'
import type { Repo } from '../../../../shared/repo-types'
import type { RepoBackedTaskSourceProvider } from '../../../../shared/task-source-repo-host-match'

/**
* Selected repos a source never queried because their origin is on the other forge.
* Why collapsed and without Retry: a retry cannot change the answer (#16473).
*/
export function TaskPageSkippedReposNotice({
provider,
repos
}: {
provider: RepoBackedTaskSourceProvider
repos: readonly Repo[]
}): React.JSX.Element | null {
const [open, setOpen] = useState(false)
if (repos.length === 0) {
return null
}
const summary =
provider === 'github'
? translate(
'auto.components.TaskPage.skippedReposHostedOnGitLab',
'Skipped — hosted on GitLab ({{value0}})',
{ value0: repos.length }
)
: translate(
'auto.components.TaskPage.skippedReposHostedOnGitHub',
'Skipped — hosted on GitHub ({{value0}})',
{ value0: repos.length }
)
return (
<div className="border-b border-border/50 px-2.5 py-1 text-muted-foreground">
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger asChild>
<Button type="button" variant="ghost" size="xs">
{open ? <ChevronDown /> : <ChevronRight />}
{summary}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="flex flex-wrap gap-x-3 gap-y-1 pb-1.5 pl-5.5 pt-0.5 font-mono text-xs">
{repos.map((repo) => (
<li key={repo.id}>{repo.displayName || repo.path}</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
</div>
)
}
Loading