Skip to content
Open
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
51 changes: 51 additions & 0 deletions src/utils/__tests__/git-review-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface PrCacheHarness {
setOriginRemoteUrl: (url: string) => void;
setGlabAvailable: (available: boolean) => void;
setCliAuthedForHost: (cli: 'gh' | 'glab', host: string, authed: boolean) => void;
setSshHostAlias: (host: string, hostname: string) => void;
}

function createHarness(): PrCacheHarness {
Expand All @@ -39,6 +40,7 @@ function createHarness(): PrCacheHarness {
gh: new Set(),
glab: new Set()
};
const sshHostAliases = new Map<string, string>();

const deps: GitReviewCacheDeps = {
execFileSync: ((cmd, args, options) => {
Expand All @@ -63,6 +65,12 @@ function createHarness(): PrCacheHarness {
return `${currentRef}\n`;
if (cmd === 'git' && commandArgs[0] === 'rev-parse')
return 'abc123\n';
if (cmd === 'ssh' && commandArgs[0] === '-G') {
const host = commandArgs[1];
if (!host)
throw new Error('missing ssh host');
return `hostname ${sshHostAliases.get(host) ?? host}\n`;
}
if (cmd === 'gh' && commandArgs[0] === '--version')
return 'gh version 2.0.0\n';
if (cmd === 'gh' && commandArgs[0] === 'auth' && commandArgs[1] === 'status') {
Expand Down Expand Up @@ -141,6 +149,9 @@ function createHarness(): PrCacheHarness {
} else {
authedHosts[cli].delete(host);
}
},
setSshHostAlias: (host: string, hostname: string) => {
sshHostAliases.set(host, hostname);
}
};
}
Expand Down Expand Up @@ -326,6 +337,46 @@ describe('git-review-cache', () => {
expect(ghPrCalls[1]?.args).toContain('feature/cache-a');
});

it('resolves SSH host aliases before selecting GitHub and pinning --repo', () => {
const harness = createHarness();
harness.setOriginRemoteUrl('git@mygit:owner/repo.git');
harness.setSshHostAlias('mygit', 'github.com');
harness.ghResponses.push('');
harness.ghResponses.push(JSON.stringify({
number: 1485,
reviewDecision: '',
state: 'OPEN',
title: 'Alias PR',
url: 'https://github.com/owner/repo/pull/1485'
}));

expect(fetchGitReviewData('/tmp/repo', harness.deps)).toEqual({
number: 1485,
provider: 'gh',
reviewDecision: '',
state: 'OPEN',
title: 'Alias PR',
url: 'https://github.com/owner/repo/pull/1485'
});

const sshCalls = harness.execCalls.filter(call => call.cmd === 'ssh');
expect(sshCalls.length).toBeGreaterThan(0);
expect(sshCalls.every(call => call.args.join(' ') === '-G mygit')).toBe(true);

const ghAuthCalls = harness.execCalls.filter(
call => call.cmd === 'gh' && call.args[0] === 'auth'
);
expect(ghAuthCalls).toHaveLength(0);

const ghPrCalls = harness.execCalls.filter(
call => call.cmd === 'gh' && call.args[0] === 'pr'
);
expect(ghPrCalls).toHaveLength(2);
expect(ghPrCalls[0]?.args).not.toContain('--repo');
expect(ghPrCalls[1]?.args).toContain('--repo');
expect(ghPrCalls[1]?.args).toContain('https://github.com/owner/repo');
});

it('falls back to --repo <origin> for forked GitLab repos when glab\'s default resolves elsewhere', () => {
const harness = createHarness();
harness.setOriginRemoteUrl('git@gitlab.com:fork-owner/example-fork.git');
Expand Down
40 changes: 36 additions & 4 deletions src/utils/git-review-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,26 +140,58 @@ function getOriginUrl(cwd: string, deps: GitReviewCacheDeps): string | null {
return url.length > 0 ? url : null;
}

function isSshRemoteUrl(url: string): boolean {
const trimmed = url.trim().toLowerCase();
return trimmed.startsWith('ssh://') || !trimmed.includes('://');
}

function resolveSshHostAlias(host: string, deps: GitReviewCacheDeps): string {
try {
const output = deps.execFileSync('ssh', ['-G', host], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
timeout: CLI_TIMEOUT,
windowsHide: true
}).trim();

for (const line of output.split(/\r?\n/)) {
const match = /^hostname\s+(.+)$/i.exec(line.trim());
if (match?.[1]) {
return match[1].toLowerCase();
}
}
} catch {
// Leave the parsed remote host unchanged when ssh is unavailable or
// cannot resolve the alias.
}

return host.toLowerCase();
}

function getEffectiveRemoteHost(url: string, host: string, deps: GitReviewCacheDeps): string {
return isSshRemoteUrl(url) ? resolveSshHostAlias(host, deps) : host.toLowerCase();
}

function getOriginHost(cwd: string, deps: GitReviewCacheDeps): string | null {
const url = getOriginUrl(cwd, deps);
if (!url) {
return null;
}
const parsed = parseRemoteUrl(url);
return parsed ? parsed.host.toLowerCase() : null;
return parsed ? getEffectiveRemoteHost(url, parsed.host, deps) : null;
}

function toHttpsRepoRef(url: string): string | null {
function toHttpsRepoRef(url: string, deps: GitReviewCacheDeps): string | null {
const parsed = parseRemoteUrl(url);
if (!parsed) {
return null;
}
return `https://${parsed.host}/${parsed.owner}/${parsed.repo}`;
return `https://${getEffectiveRemoteHost(url, parsed.host, deps)}/${parsed.owner}/${parsed.repo}`;
}

function getOriginRepoRef(cwd: string, deps: GitReviewCacheDeps): string | null {
const url = getOriginUrl(cwd, deps);
return url ? toHttpsRepoRef(url) : null;
return url ? toHttpsRepoRef(url, deps) : null;
}

// Self-hosted hosts that name neither forge are resolved by probing each CLI's
Expand Down