Skip to content
Merged
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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,14 @@
"type": "string",
"description": "Default branch name to use if it cannot be determined dynamically"
},
"copyGithubUrl.domainOverride": {
"type": "string",
"description": "GitHub domain override, for scenarios like enterprise instances or SSH aliases. E.g. github.example.com"
},
"copyGithubUrl.gitUrl": {
"type": "string",
"description": "The github domain. Eg: github.example.com"
"description": "Deprecated: Use domainOverride instead. GitHub domain override, for scenarios like enterprise instances or SSH aliases.",
"deprecationMessage": "This setting is deprecated. Please use copyGithubUrl.domainOverride instead."
},
"copyGithubUrl.rootGitFolder": {
"type": "string",
Expand Down
38 changes: 20 additions & 18 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ async function getGithubUrl (editor, type = {}) {
*/
async function getDefaultBranch (repository) {
try {
// 1. Try reading .git config
// 1. Try user configuration
const extensionConfig = vscode.workspace.getConfiguration('copyGithubUrl')
const defaultBranchFallback = extensionConfig.get('defaultBranchFallback')
if (defaultBranchFallback) return defaultBranchFallback

// 2. Try reading .git config
try {
const configPath = path.join(repository.rootUri.fsPath, '.git', 'config')
const gitConfig = await fs.readFile(configPath, 'utf8')
Expand All @@ -87,7 +92,7 @@ async function getDefaultBranch (repository) {
if (!isTestEnvironment) console.error('Failed to read git config:', error)
}

// 2. Try git branch -r
// 3. Try git branch -r
const MAX_RETRIES = BRANCH_DISCOVERY_MAX_RETRIES
const RETRY_DELAY = BRANCH_DISCOVERY_RETRY_DELAY
try {
Expand Down Expand Up @@ -144,11 +149,6 @@ async function getDefaultBranch (repository) {
if (!isTestEnvironment) console.error('Failed to run git branch -r:', error)
}

// 3. Try user configuration
const extensionConfig = vscode.workspace.getConfiguration('copyGithubUrl')
const defaultBranchFallback = extensionConfig.get('defaultBranchFallback')
if (defaultBranchFallback) return defaultBranchFallback

throw new Error('Could not determine default branch. Configure copyGithubUrl.defaultBranchFallback in settings.')
} catch (error) {
if (error.message.includes('Configure copyGithubUrl.defaultBranchFallback')) throw error
Expand All @@ -163,7 +163,8 @@ async function getDefaultBranch (repository) {
*/
async function getGithubUrlFromRemotes (repository) {
const config = vscode.workspace.getConfiguration('copyGithubUrl')
const gitUrl = config.get('gitUrl')
// Check domainOverride first, fall back to gitUrl for backwards compatibility
const domainOverride = config.get('domainOverride') || config.get('gitUrl')
const remotes = repository.state.remotes

// Try to get the remote for the current branch first
Expand All @@ -175,20 +176,18 @@ async function getGithubUrlFromRemotes (repository) {
if (branchConfig) {
const remote = remotes.find(r => r.name === branchConfig.remote)
if (remote) {
// Always include both gitUrl and remote domain as extraBaseUrls
const domain = remote.fetchUrl.match(/(?:https?:\/\/|git@|ssh:\/\/(?:[^@]+@)?)([^:/]+)/)?.[1]
return Promise.resolve(githubUrlFromGit(remote.fetchUrl, {
extraBaseUrls: [gitUrl, domain].filter(Boolean)
}))
// If gitUrl is configured, only use that as the base URL
const extraBaseUrls = domainOverride ? [domainOverride] : [remote.fetchUrl.match(/(?:https?:\/\/|git@|ssh:\/\/(?:[^@]+@)?)([^:/]+)/)?.[1]].filter(Boolean)
return Promise.resolve(githubUrlFromGit(remote.fetchUrl, { extraBaseUrls }))
}
}
}

// If gitUrl is configured, look for that specific domain next
if (gitUrl) {
const enterpriseRemote = remotes.find(r => r.fetchUrl.toLowerCase().includes(gitUrl.toLowerCase()))
// If domainOverride is configured, look for that specific domain next
if (domainOverride) {
const enterpriseRemote = remotes.find(r => r.fetchUrl.toLowerCase().includes(domainOverride.toLowerCase()))
if (enterpriseRemote) {
return Promise.resolve(githubUrlFromGit(enterpriseRemote.fetchUrl, { extraBaseUrls: [gitUrl] }))
return Promise.resolve(githubUrlFromGit(enterpriseRemote.fetchUrl, { extraBaseUrls: [domainOverride] }))
}
}

Expand All @@ -198,7 +197,10 @@ async function getGithubUrlFromRemotes (repository) {
const domain = remote.fetchUrl.match(/(?:https?:\/\/|git@|ssh:\/\/(?:[^@]+@)?)([^:/]+)/)?.[1]
if (!domain) continue

const url = githubUrlFromGit(remote.fetchUrl, { extraBaseUrls: [gitUrl, domain].filter(Boolean) })
const normalizedUrl = domainOverride
? remote.fetchUrl.replace(domain, domainOverride)
: remote.fetchUrl
const url = githubUrlFromGit(normalizedUrl, { extraBaseUrls: [domain].filter(Boolean) })
if (url) return Promise.resolve(url)
} catch (error) {
if (!isTestEnvironment) console.warn(`Failed to process remote ${remote.name}: ${error.message}`)
Expand Down
88 changes: 88 additions & 0 deletions test/unit/github.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -789,4 +789,92 @@ origin/feature/123
clock.restore()
}
})

test('getGithubUrlFromRemotes should use domainOverride over gitUrl', async function () {
const repository = {
state: {
HEAD: { name: 'main' },
refs: [],
remotes: [{ name: 'origin', fetchUrl: 'git@foo_bar:user/repo.git' }]
}
}

sandbox.stub(vscode.workspace, 'getConfiguration').returns({
get: (key) => {
if (key === 'domainOverride') return 'github.com'
if (key === 'gitUrl') return 'other.com'
return undefined
}
})

const url = await _main.getGithubUrlFromRemotes(repository)
assert.strictEqual(url, 'https://github.com/user/repo')
})

test('getGithubUrlFromRemotes should fallback to gitUrl when domainOverride not set', async function () {
const repository = {
state: {
HEAD: { name: 'main' },
refs: [],
remotes: [{ name: 'origin', fetchUrl: 'git@foo_bar:user/repo.git' }]
}
}

sandbox.stub(vscode.workspace, 'getConfiguration').returns({
get: (key) => {
if (key === 'domainOverride') return undefined
if (key === 'gitUrl') return 'github.com'
return undefined
}
})

const url = await _main.getGithubUrlFromRemotes(repository)
assert.strictEqual(url, 'https://github.com/user/repo')
})

test('getDefaultBranch should prioritize defaultBranchFallback configuration', async function () {
const repository = {
rootUri: { fsPath: '/test/path' }
}

// Configure both fallback and git config
sandbox.stub(vscode.workspace, 'getConfiguration').returns({
get: (key) => key === 'defaultBranchFallback' ? 'custom-branch' : undefined
})

sandbox.stub(fs.promises, 'readFile').resolves(`
[branch "main"]
remote = origin
merge = refs/heads/main
`)

const branch = await _main.getDefaultBranch(repository)
assert.strictEqual(branch, 'custom-branch', 'Should use defaultBranchFallback even when git config exists')
})

test('getDefaultBranch should follow correct fallback chain', async function () {
const repository = {
rootUri: { fsPath: '/test/path' }
}

// No defaultBranchFallback configured
sandbox.stub(vscode.workspace, 'getConfiguration').returns({
get: () => undefined
})

// Mock git config with main branch
sandbox.stub(fs.promises, 'readFile').resolves(`
[branch "main"]
remote = origin
merge = refs/heads/main
`)

// Mock git branch -r command
sandbox.stub(cp, 'exec').callsFake((cmd, opts, callback) => {
callback(null, 'origin/HEAD -> origin/develop\norigin/main\norigin/develop')
})

const branch = await _main.getDefaultBranch(repository)
assert.strictEqual(branch, 'main', 'Should fall back to git config when no defaultBranchFallback')
})
})