From 8604a4d570575ff7ecde73e7b5fe43415ca696ca Mon Sep 17 00:00:00 2001 From: Pr1p <48266347+Pr1p@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:43:39 +0800 Subject: [PATCH] fix(bitbucket): recognize ssh.bitbucket.org remotes (#31002) * fix(bitbucket): recognize the SSH host alias * fix(bitbucket): recognize the SSH host alias on current main Apply Chen's original SSH alias fix to current main, preserving canonical Bitbucket browser links and adding case/port and lookalike-host coverage. Co-authored-by: Chen --------- Co-authored-by: Neil --- src/main/bitbucket/repository-ref.test.ts | 20 ++++++++++++++++++++ src/main/bitbucket/repository-ref.ts | 5 +++-- src/main/git/hosted-remote-url.test.ts | 19 +++++++++++++++++++ src/main/git/hosted-remote-url.ts | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/main/bitbucket/repository-ref.test.ts b/src/main/bitbucket/repository-ref.test.ts index 2831efae16c..63b04c05232 100644 --- a/src/main/bitbucket/repository-ref.test.ts +++ b/src/main/bitbucket/repository-ref.test.ts @@ -37,9 +37,29 @@ describe('Bitbucket repository refs', () => { workspace: 'team', repoSlug: 'project' }) + expect(parseBitbucketRepoRef('git@ssh.bitbucket.org:team/project.git')).toEqual({ + workspace: 'team', + repoSlug: 'project' + }) + expect(parseBitbucketRepoRef('ssh://git@ssh.bitbucket.org/team/project.git')).toEqual({ + workspace: 'team', + repoSlug: 'project' + }) expect(parseBitbucketRepoRef('https://github.com/team/project.git')).toBeNull() }) + it('recognizes the SSH host case-insensitively without accepting lookalike hosts', () => { + expect(parseBitbucketRepoRef('git@SSH.BITBUCKET.ORG:team/project.git')).toEqual({ + workspace: 'team', + repoSlug: 'project' + }) + expect(parseBitbucketRepoRef('git@ssh.bitbucket.org.example.com:team/project.git')).toBeNull() + expect( + parseBitbucketRepoRef('ssh://git@ssh.bitbucket.org.example.com/team/project.git') + ).toBeNull() + expect(parseBitbucketRepoRef('git@notssh.bitbucket.org:team/project.git')).toBeNull() + }) + it('strips trailing slashes after .git suffixes', () => { expect(parseBitbucketRepoRef('https://bitbucket.org/team/project.git/')).toEqual({ workspace: 'team', diff --git a/src/main/bitbucket/repository-ref.ts b/src/main/bitbucket/repository-ref.ts index 8456b2d4ae3..8a0c35ff986 100644 --- a/src/main/bitbucket/repository-ref.ts +++ b/src/main/bitbucket/repository-ref.ts @@ -51,14 +51,15 @@ function parseBitbucketPath(pathname: string): BitbucketRepoRef | null { export function parseBitbucketRepoRef(remoteUrl: string): BitbucketRepoRef | null { const trimmed = remoteUrl.trim() - const scpLike = trimmed.match(/^(?:[^@]+@)?bitbucket\.org:([^\s]+?)(?:\.git)?$/i) + const scpLike = trimmed.match(/^(?:[^@]+@)?(?:ssh\.)?bitbucket\.org:([^\s]+?)(?:\.git)?$/i) if (scpLike) { return parseBitbucketPath(scpLike[1]) } try { const url = new URL(trimmed) - if (url.hostname.toLowerCase() !== 'bitbucket.org') { + const host = url.hostname.toLowerCase() + if (host !== 'bitbucket.org' && host !== 'ssh.bitbucket.org') { return null } return parseBitbucketPath(url.pathname) diff --git a/src/main/git/hosted-remote-url.test.ts b/src/main/git/hosted-remote-url.test.ts index 2dec525812b..c8c2b78da43 100644 --- a/src/main/git/hosted-remote-url.test.ts +++ b/src/main/git/hosted-remote-url.test.ts @@ -55,6 +55,16 @@ describe('hosted remote URLs', () => { path: 'team/repo', provider: 'bitbucket' }) + expect(parseHostedRemote('git@ssh.bitbucket.org:team/repo.git')).toEqual({ + host: 'bitbucket.org', + path: 'team/repo', + provider: 'bitbucket' + }) + expect(parseHostedRemote('ssh://git@ssh.bitbucket.org/team/repo.git')).toEqual({ + host: 'bitbucket.org', + path: 'team/repo', + provider: 'bitbucket' + }) }) it('builds file URLs with encoded branches and paths', () => { @@ -86,6 +96,9 @@ describe('hosted remote URLs', () => { 5 ) ).toBe('https://github.com/Org/Repo/blob/feature%2Fx/src/a.ts#L5') + expect( + buildHostedRemoteFileUrl('git@ssh.bitbucket.org:team/repo.git', 'src/a.ts', 'feature/x', 5) + ).toBe('https://bitbucket.org/team/repo/src/feature%2Fx/src/a.ts#a.ts-5') }) it('builds Bitbucket line fragments from the target file name', () => { @@ -105,6 +118,9 @@ describe('hosted remote URLs', () => { expect(buildHostedRemoteCommitUrl('git@bitbucket.org:team/repo.git', sha)).toBe( `https://bitbucket.org/team/repo/commits/${sha}` ) + expect(buildHostedRemoteCommitUrl('ssh://git@SSH.BITBUCKET.ORG:443/team/repo.git', sha)).toBe( + `https://bitbucket.org/team/repo/commits/${sha}` + ) expect(buildHostedRemoteCommitUrl('git@ALTSSH.GITLAB.COM:team/subgroup/project.git', sha)).toBe( `https://gitlab.com/team/subgroup/project/-/commit/${sha}` ) @@ -124,6 +140,9 @@ describe('hosted remote URLs', () => { expect(parseHostedRemote('git@example.com:team/repo.git')).toBeNull() expect(parseHostedRemote('git@github.com:repo.git')).toBeNull() expect(parseHostedRemote('ftp://github.com/Org/Repo.git')).toBeNull() + expect(parseHostedRemote('git@ssh.bitbucket.org.example.com:team/repo.git')).toBeNull() + expect(parseHostedRemote('ssh://git@ssh.bitbucket.org.example.com/team/repo.git')).toBeNull() + expect(parseHostedRemote('git@notssh.bitbucket.org:team/repo.git')).toBeNull() expect(parseHostedRemote('git@altssh.gitlab.com.example.com:team/repo.git')).toBeNull() expect(parseHostedRemote('ssh://git@altssh.gitlab.com.example.com/team/repo.git')).toBeNull() expect(parseHostedRemote('git@notaltssh.gitlab.com:team/repo.git')).toBeNull() diff --git a/src/main/git/hosted-remote-url.ts b/src/main/git/hosted-remote-url.ts index 349d911cf5c..7773dc94bcc 100644 --- a/src/main/git/hosted-remote-url.ts +++ b/src/main/git/hosted-remote-url.ts @@ -30,7 +30,7 @@ function providerForHost(host: string): HostedRemoteHost | null { if (normalized === 'gitlab.com') { return { host: 'gitlab.com', provider: 'gitlab' } } - if (normalized === 'bitbucket.org') { + if (normalized === 'bitbucket.org' || normalized === 'ssh.bitbucket.org') { return { host: 'bitbucket.org', provider: 'bitbucket' } } return null