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:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user