fix(ui): support bare scp and IPv6 host forms in git provider detection

Extract a single shared parseGitHost (packages/ui/src/lib/gitHost.ts) used
by both the custom-domain registry (normalizeProviderDomain) and remote
detection (gitProvider). Previously both parsers required a user@ prefix,
rejecting valid bare scp remotes like codeberg.org:owner/repo.git, and
mangled IPv6 hosts. Now scp forms with or without a user, ssh:// URLs with
ports, git+ssh scheme, and bracketed/unbracketed IPv6 all normalize to a
bare hostname; Windows-path-like input is rejected.
This commit is contained in:
2026-08-16 16:27:50 +00:00
parent ca91fd7e2d
commit b772f5dad9
6 changed files with 206 additions and 46 deletions
@@ -0,0 +1,39 @@
import { describe, expect, test } from 'bun:test';
import { useGitProviderDomainsStore } from './useGitProviderDomainsStore';
const resetDomains = () => {
useGitProviderDomainsStore.setState({
domains: { github: [], gitlab: [], gitea: [] },
});
};
describe('useGitProviderDomainsStore', () => {
test('normalizes and dedupes custom domains into bare hostnames', () => {
resetDomains();
useGitProviderDomainsStore.getState().setDomains('gitea', [
'git@codeberg.org:owner/repo.git',
'ssh://git@gitea.example.com:2222/o/r.git',
'https://g.example.com/x',
' git.example.org ',
]);
expect(useGitProviderDomainsStore.getState().domains.gitea).toEqual([
'codeberg.org',
'gitea.example.com',
'g.example.com',
'git.example.org',
]);
});
test('drops unparseable and duplicate entries', () => {
resetDomains();
useGitProviderDomainsStore.getState().setDomains('gitlab', [
'git@gitlab.example.com:group/repo.git',
'git@gitlab.example.com:group/repo.git',
'',
' ',
'not a url',
'C:\\foo',
]);
expect(useGitProviderDomainsStore.getState().domains.gitlab).toEqual(['gitlab.example.com']);
});
});
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { parseGitHost } from '@/lib/gitHost';
import { createDeferredSafeJSONStorage } from './utils/safeStorage';
export type GitProviderName = 'github' | 'gitlab' | 'gitea';
@@ -20,30 +21,11 @@ const EMPTY_DOMAINS: GitProviderDomains = { github: [], gitlab: [], gitea: [] };
/**
* Normalize a raw user-supplied domain into a bare hostname. Accepts plain
* hostnames, URLs (scheme/port/path stripped), and scp-like git remotes
* (`git@host:owner/repo.git`). Returns null for empty or unparseable input.
* hostnames, URLs (scheme/port/path stripped), and scp-like git remotes with
* or without a user prefix (`git@host:owner/repo.git`, `host:owner/repo.git`).
* Returns null for empty or unparseable input.
*/
export const normalizeProviderDomain = (raw: string): string | null => {
const value = (raw ?? '').trim();
if (!value) {
return null;
}
// scp-like form: git@host:owner/repo.git
const at = value.indexOf('@');
if (at >= 0) {
const rest = value.slice(at + 1);
const colon = rest.indexOf(':');
if (colon > 0 && !rest.slice(0, colon).includes('/')) {
return rest.slice(0, colon).toLowerCase().replace(/\.$/, '');
}
}
try {
const parsed = new URL(value.includes('://') ? value : `ssh://${value}`);
return parsed.hostname.toLowerCase().replace(/\.$/, '');
} catch {
return null;
}
};
export const normalizeProviderDomain = (raw: string): string | null => parseGitHost(raw);
const normalizeDomainList = (entries: unknown): string[] => {
if (!Array.isArray(entries)) {