mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* fix(gitlab): port-aware self-hosted host recognition
Use the URL host (including a non-default web/API port) as the GitLab
host identity instead of the port-less hostname, and match known hosts
port-aware:
- A known-host entry without a port matches any port of the same
hostname (preserves legacy bare-host and gitlab.com recognition).
- A known-host entry WITH a port matches only that exact host:port, so
two services sharing a hostname on different ports (e.g. a GitLab and
a Gitea) are no longer conflated.
- For ssh/git remotes the port is a transport port (e.g. ssh :2222) and
is dropped; for http(s) remotes the port is the endpoint and kept.
- Also capture an optional :port in parseGlabAuthStatusHosts so a
self-hosted GitLab on a non-default port is discovered correctly.
* fix(gitlab): per-connection known-hosts cache + port-aware auth-status parsing
getGlabKnownHosts() was connection-blind and cached process-globally,
and on any failure it cached [gitlab.com] forever — so a repo on an SSH
connection never discovered its self-hosted host once a probe failed
before the tunnel was ready.
- getGlabKnownHosts(connectionId?) now caches per connection so a
connected repo's authenticated hosts don't leak into the local
context (or vice versa).
- The failure fallback (canonical default) is no longer cached, so a
later probe can re-discover the real host once auth/tunnel is ready.
- parseGlabAuthStatusHosts captures an optional :port on both the
'Logged in to <host>' and header-style lines, keeping two services on
the same hostname distinct by port.
* fix(gitlab): isolate unresolvable projects instead of cwd-fallback that hits exit 128
listIssues/getIssue fell back to an unscoped 'glab issue list' / 'glab
issue view' that infers the project from cwd. For a repo on an SSH
connection cwd is not the repo dir, so glab runs git resolution in a
non-repo dir and fails with 'git: exit status 128'. In an 'All projects'
aggregate one such failure could sink the whole issues panel.
When a projectRef cannot be resolved, return a structured, isolated
per-project result (listIssues: { items: [], error: not_found };
getIssue: null) and spawn no glab subprocess. Behavior is unchanged when
a projectRef IS resolved (the scoped '-R' / 'api projects/...' path).
* fix(gitlab): recognize modern /-/work_items/<iid> issue URLs
Modern GitLab emits issue URLs as /-/work_items/<iid> in addition to the
legacy /-/issues/<iid>. The URL classifiers only matched /-/issues/, so
work-item-form issue links went unrecognized.
Extend the gitlab-links parsers (parseGitLabIssueOrMRNumber /
parseGitLabIssueOrMRLink, which also backs isWorkItemLookupText) and
isGitLabIssueUrl to accept /-/work_items/<iid>, mapping it to an issue
work item with the same project-path + iid extraction.
* fix(gitlab): thread connectionId into getGlabKnownHosts call sites
Follow the existing connectionId-threading pattern: pass the repo's
connectionId into every getGlabKnownHosts() call (client.ts,
work-item-details.ts, orca-runtime.ts) so the per-connection known-hosts
cache is keyed correctly and self-hosted hosts are discovered against
the right glab context.
* docs(gitlab): use generic example hosts in comments
* fix(gitlab): pass self-hosted host:port via GITLAB_HOST (glab --hostname rejects ports)
* polish: satisfy oxlint curly + oxfmt on merged gitlab port-recognition code
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Ptah-CT <auctor@xinfty.space>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { parseGitLabProjectRef, parseRemoteProjectRefCandidate } from './project-ref-parser'
|
|
|
|
describe('gitlab project ref parsing', () => {
|
|
it('parses HTTPS and SSH GitLab.com remotes', () => {
|
|
expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'acme/widgets'
|
|
})
|
|
expect(parseGitLabProjectRef('git@gitlab.com:stablyai/orca.git')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'stablyai/orca'
|
|
})
|
|
})
|
|
|
|
it('preserves nested group paths', () => {
|
|
expect(parseGitLabProjectRef('git@gitlab.com:group/subgroup/project.git')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'group/subgroup/project'
|
|
})
|
|
expect(parseGitLabProjectRef('https://gitlab.com/g1/g2/g3/proj.git')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'g1/g2/g3/proj'
|
|
})
|
|
})
|
|
|
|
it('returns null for non-GitLab hosts when host not in knownHosts', () => {
|
|
expect(parseGitLabProjectRef('git@github.com:stablyai/orca.git')).toBeNull()
|
|
expect(parseGitLabProjectRef('git@example.com:foo/bar.git')).toBeNull()
|
|
})
|
|
|
|
it('matches self-hosted hosts when included in knownHosts', () => {
|
|
expect(
|
|
parseGitLabProjectRef('git@gitlab.example.com:team/api.git', [
|
|
'gitlab.com',
|
|
'gitlab.example.com'
|
|
])
|
|
).toEqual({ host: 'gitlab.example.com', path: 'team/api' })
|
|
})
|
|
|
|
it('drops the SSH transport port from the GitLab host identity', () => {
|
|
// Why: for ssh remotes the port is a transport port (e.g. :2222), not the
|
|
// web/API endpoint, so it must not become part of the recognized host.
|
|
expect(
|
|
parseGitLabProjectRef('ssh://git@gitlab.example.com:2222/team/api.git', [
|
|
'gitlab.com',
|
|
'gitlab.example.com'
|
|
])
|
|
).toEqual({ host: 'gitlab.example.com', path: 'team/api' })
|
|
})
|
|
|
|
it('keeps the HTTP(S) port as part of the self-hosted GitLab host identity', () => {
|
|
// Why: a self-hosted GitLab served on a non-default web port (e.g. :8443)
|
|
// is identified by host:port end-to-end so `glab --hostname` targets it.
|
|
expect(
|
|
parseGitLabProjectRef('https://gitlab.example.com:8443/team/api.git', [
|
|
'gitlab.com',
|
|
'gitlab.example.com:8443'
|
|
])
|
|
).toEqual({ host: 'gitlab.example.com:8443', path: 'team/api' })
|
|
})
|
|
|
|
it('matches a port-bearing http remote against a port-less legacy known host', () => {
|
|
// Why: a known host recorded without a port (legacy/bare entry) still
|
|
// recognizes a remote on any port of the same hostname.
|
|
expect(
|
|
parseGitLabProjectRef('https://gitlab.example.com:8443/team/api.git', [
|
|
'gitlab.com',
|
|
'gitlab.example.com'
|
|
])
|
|
).toEqual({ host: 'gitlab.example.com:8443', path: 'team/api' })
|
|
})
|
|
|
|
it('distinguishes two services on the same host by port — only the GitLab one matches', () => {
|
|
// Why: a GitLab on :8443 and a Gitea on :3030 share a hostname but are
|
|
// different services. With only the GitLab port in known hosts, the Gitea
|
|
// remote must NOT be classified as GitLab.
|
|
const knownHosts = ['gitlab.com', 'gitea.example.com:8443']
|
|
expect(parseGitLabProjectRef('http://gitea.example.com:8443/team/api.git', knownHosts)).toEqual(
|
|
{ host: 'gitea.example.com:8443', path: 'team/api' }
|
|
)
|
|
expect(
|
|
parseGitLabProjectRef('http://gitea.example.com:3030/team/api.git', knownHosts)
|
|
).toBeNull()
|
|
})
|
|
|
|
it('matches an SCP-like self-hosted remote against a port-less known host', () => {
|
|
expect(
|
|
parseGitLabProjectRef('git@gitlab.example.com:team/api.git', [
|
|
'gitlab.com',
|
|
'gitlab.example.com'
|
|
])
|
|
).toEqual({ host: 'gitlab.example.com', path: 'team/api' })
|
|
})
|
|
|
|
it('keeps gitlab.com (no port) recognized as a default host', () => {
|
|
expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'acme/widgets'
|
|
})
|
|
})
|
|
|
|
it('rejects single-segment paths (host root or user-only)', () => {
|
|
expect(parseGitLabProjectRef('git@gitlab.com:foo.git')).toBeNull()
|
|
expect(parseGitLabProjectRef('https://gitlab.com/foo.git')).toBeNull()
|
|
})
|
|
|
|
it('handles missing .git suffix', () => {
|
|
expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'acme/widgets'
|
|
})
|
|
})
|
|
|
|
it('strips trailing slashes after .git suffixes', () => {
|
|
expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git/')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'acme/widgets'
|
|
})
|
|
expect(parseGitLabProjectRef('ssh://git@gitlab.com/acme/widgets.git/')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'acme/widgets'
|
|
})
|
|
})
|
|
|
|
it('preserves git protocol remote support', () => {
|
|
expect(parseGitLabProjectRef('git://gitlab.com/acme/widgets.git')).toEqual({
|
|
host: 'gitlab.com',
|
|
path: 'acme/widgets'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('gitlab remote project ref candidates', () => {
|
|
it('extracts self-hosted candidates before the host is trusted', () => {
|
|
expect(parseRemoteProjectRefCandidate('git@gitlab.internal:team/orca.git')).toEqual({
|
|
host: 'gitlab.internal',
|
|
path: 'team/orca'
|
|
})
|
|
})
|
|
|
|
it('rejects non-git URLs and single-segment project paths', () => {
|
|
expect(parseRemoteProjectRefCandidate('file:///tmp/repo')).toBeNull()
|
|
expect(parseRemoteProjectRefCandidate('git@gitlab.internal:team.git')).toBeNull()
|
|
})
|
|
})
|