Skip to content

Commit cc737a6

Browse files
Copilotalexr00
andauthored
Fix extension handling of git URL aliasing in config (#8245)
* Initial plan * Changes before error encountered Co-authored-by: alexr00 <[email protected]> * Implement git URL alias resolution for remote parsing - Add resolveGitUrl function to resolve git URL aliases (e.g., "gh:" -> "[email protected]:") - Add parseRepositoryRemotesAsync function to parse remotes with alias resolution - Update computeAllUnknownRemotes and computeAllGitHubRemotes to use async parsing - Fixes issue where extension mistakenly thinks user is using GH Enterprise with git URL aliasing Co-authored-by: alexr00 <[email protected]> * Fix extension handling of git URL aliasing in config Co-authored-by: alexr00 <[email protected]> * Increase log visibility --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexr00 <[email protected]>
1 parent ddfaf19 commit cc737a6

File tree

2 files changed

+84
-3
lines changed

2 files changed

+84
-3
lines changed

src/common/remote.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { AuthProvider, GitHubServerType } from './authentication';
7+
import Logger from './logger';
78
import { Protocol } from './protocol';
89
import { Repository } from '../api/api';
910
import { getEnterpriseUri, isEnterprise } from '../github/utils';
@@ -74,6 +75,54 @@ export function parseRemote(remoteName: string, url: string, originalProtocol?:
7475
return null;
7576
}
7677

78+
/**
79+
* Resolves git URL aliases by applying insteadOf substitutions from git config.
80+
* For example, if git config has:
81+
* [url "[email protected]:"]
82+
* insteadOf = "gh:"
83+
* Then "gh:user/repo" will be resolved to "[email protected]:user/repo"
84+
*
85+
* @param url The URL to resolve
86+
* @param repository The repository to get config from
87+
* @returns The resolved URL, or the original URL if no substitution found
88+
*/
89+
async function resolveGitUrl(url: string, repository: Repository): Promise<string> {
90+
try {
91+
// Get all git config entries
92+
const configs = await repository.getConfigs();
93+
94+
// Find all url.*.insteadOf entries
95+
const urlSubstitutions: { prefix: string; replacement: string }[] = [];
96+
97+
for (const config of configs) {
98+
// Match patterns like "url.https://github.com/.insteadOf" or "[email protected]:.insteadOf"
99+
const match = config.key.match(/^url\.(.+)\.insteadof$/i);
100+
if (match) {
101+
const replacement = match[1];
102+
const prefix = config.value;
103+
urlSubstitutions.push({ prefix, replacement });
104+
}
105+
}
106+
107+
// Sort by prefix length (longest first) to handle overlapping prefixes correctly
108+
urlSubstitutions.sort((a, b) => b.prefix.length - a.prefix.length);
109+
110+
// Apply the first matching substitution
111+
for (const { prefix, replacement } of urlSubstitutions) {
112+
if (url.startsWith(prefix)) {
113+
const resolvedUrl = replacement + url.substring(prefix.length);
114+
Logger.appendLine(`Resolved git URL alias: "${url}" -> "${resolvedUrl}"`, 'Remote');
115+
return resolvedUrl;
116+
}
117+
}
118+
} catch (error) {
119+
Logger.error(`Failed to resolve git URL aliases for "${url}": ${error}`, 'Remote');
120+
}
121+
122+
// No substitution found or error occurred, return original URL
123+
return url;
124+
}
125+
77126
export function parseRepositoryRemotes(repository: Repository): Remote[] {
78127
const remotes: Remote[] = [];
79128
for (const r of repository.state.remotes) {
@@ -94,6 +143,38 @@ export function parseRepositoryRemotes(repository: Repository): Remote[] {
94143
return remotes;
95144
}
96145

146+
/**
147+
* Asynchronously parses repository remotes with git URL alias resolution.
148+
* This version resolves git URL aliases (e.g., "gh:" -> "[email protected]:") before parsing.
149+
* Use this version when you need accurate remote parsing with alias resolution.
150+
*
151+
* @param repository The repository to parse remotes from
152+
* @returns Promise resolving to array of Remote objects
153+
*/
154+
export async function parseRepositoryRemotesAsync(repository: Repository): Promise<Remote[]> {
155+
const remotes: Remote[] = [];
156+
for (const r of repository.state.remotes) {
157+
const urls: string[] = [];
158+
if (r.fetchUrl) {
159+
// Resolve git URL aliases before parsing
160+
const resolvedUrl = await resolveGitUrl(r.fetchUrl, repository);
161+
urls.push(resolvedUrl);
162+
}
163+
if (r.pushUrl && r.pushUrl !== r.fetchUrl) {
164+
// Resolve git URL aliases before parsing
165+
const resolvedUrl = await resolveGitUrl(r.pushUrl, repository);
166+
urls.push(resolvedUrl);
167+
}
168+
urls.forEach(url => {
169+
const remote = parseRemote(r.name, url);
170+
if (remote) {
171+
remotes.push(remote);
172+
}
173+
});
174+
}
175+
return remotes;
176+
}
177+
97178
export class GitHubRemote extends Remote {
98179
static remoteAsGitHub(remote: Remote, githubServerType: GitHubServerType): GitHubRemote {
99180
return new GitHubRemote(remote.remoteName, remote.url, remote.gitProtocol, githubServerType);

src/github/folderRepositoryManager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { findLocalRepoRemoteFromGitHubRef } from '../common/githubRef';
3737
import { Disposable, disposeAll } from '../common/lifecycle';
3838
import Logger from '../common/logger';
3939
import { Protocol, ProtocolType } from '../common/protocol';
40-
import { GitHubRemote, parseRemote, parseRepositoryRemotes, Remote } from '../common/remote';
40+
import { GitHubRemote, parseRemote, parseRepositoryRemotes, parseRepositoryRemotesAsync, Remote } from '../common/remote';
4141
import {
4242
ALLOW_FETCH,
4343
AUTO_STASH,
@@ -276,7 +276,7 @@ export class FolderRepositoryManager extends Disposable {
276276
}
277277

278278
public async computeAllUnknownRemotes(): Promise<Remote[]> {
279-
const remotes = parseRepositoryRemotes(this.repository);
279+
const remotes = await parseRepositoryRemotesAsync(this.repository);
280280
const potentialRemotes = remotes.filter(remote => remote.host);
281281
const serverTypes = await Promise.all(
282282
potentialRemotes.map(remote => this._githubManager.isGitHub(remote.gitProtocol.normalizeUri()!)),
@@ -297,7 +297,7 @@ export class FolderRepositoryManager extends Disposable {
297297
}
298298

299299
public async computeAllGitHubRemotes(): Promise<GitHubRemote[]> {
300-
const remotes = parseRepositoryRemotes(this.repository);
300+
const remotes = await parseRepositoryRemotesAsync(this.repository);
301301
const potentialRemotes = remotes.filter(remote => remote.host);
302302
const serverTypes = await Promise.all(
303303
potentialRemotes.map(remote => this._githubManager.isGitHub(remote.gitProtocol.normalizeUri()!)),

0 commit comments

Comments
 (0)