From 50991e9eec823b8e2c6faf1f556b2a28b886bc2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:42:22 +0000 Subject: [PATCH 1/4] Initial plan From 2b06146d6f9165b83016710dc4f18f0bb1d9925e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:12:22 +0000 Subject: [PATCH 2/4] Skip inaccessible repositories after 404 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 21 +++++++++- src/github/githubRepository.ts | 17 +++++++- .../github/folderRepositoryManager.test.ts | 34 ++++++++++++++++ src/test/github/githubRepository.test.ts | 40 +++++++++++++++++++ 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 9807c8a53e..103a906ecb 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -529,11 +529,26 @@ export class FolderRepositoryManager extends Disposable { }; return Promise.all(resolveRemotePromises).then(async (remoteResults: boolean[]) => { + const inaccessibleRepositories: GitHubRepository[] = []; const missingSaml: GitHubRepository[] = []; for (let i = 0; i < remoteResults.length; i++) { if (!remoteResults[i]) { - missingSaml.push(repositories[i]); + if (repositories[i].isInaccessible) { + inaccessibleRepositories.push(repositories[i]); + } else { + missingSaml.push(repositories[i]); + } + } + } + for (const inaccessible of inaccessibleRepositories) { + this._sessionIgnoredRemoteNames.add(inaccessible.remote.remoteName); + this._inaccessibleRepos.add(`${inaccessible.remote.owner.toLowerCase()}/${inaccessible.remote.repositoryName.toLowerCase()}`); + this.removeGitHubRepository(inaccessible.remote); + const index = repositories.indexOf(inaccessible); + if (index > -1) { + repositories.splice(index, 1); } + inaccessible.dispose(); } if (missingSaml.length > 0) { const result = await this._credentialStore.showSamlMessageAndAuth(missingSaml.map(repo => repo.remote.owner)); @@ -2955,6 +2970,10 @@ export class FolderRepositoryManager extends Disposable { private _createGitHubRepositoryBulkhead = bulkhead(1, 300); async createGitHubRepository(remote: Remote, credentialStore: CredentialStore, silent?: boolean, ignoreRemoteName: boolean = false): Promise { + const repoKey = `${remote.owner.toLowerCase()}/${remote.repositoryName.toLowerCase()}`; + if (this._inaccessibleRepos.has(repoKey)) { + throw new Error(`Repository ${remote.owner}/${remote.repositoryName} is not accessible.`); + } // Use a bulkhead/semaphore to ensure that we don't create multiple GitHubRepositories for the same remote at the same time. return this._createGitHubRepositoryBulkhead.execute(async () => { return this.findExistingGitHubRepository({ owner: remote.owner, repositoryName: remote.repositoryName, remoteName: ignoreRemoteName ? undefined : remote.remoteName }) ?? diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 4eaba914b7..fb6abd413d 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -182,6 +182,7 @@ export class GitHubRepository extends Disposable { protected _initialized: boolean = false; protected _hub: GitHub | undefined; protected _metadata: Promise | undefined; + private _isInaccessible: boolean = false; public commentsController?: vscode.CommentController; public commentsHandler?: PRCommentControllerRegistry; private _pullRequestModelsByNumber: LRUCache = new LRUCache({ @@ -207,6 +208,7 @@ export class GitHubRepository extends Disposable { private _maxItemNumberCache: { value: number; fetchedAt: number } | undefined; private _maxItemNumberPromise: Promise | undefined; get areQueriesLimited(): boolean { return this._areQueriesLimited; } + get isInaccessible(): boolean { return this._isInaccessible; } private _branchesCache: Map = new Map(); @@ -435,7 +437,13 @@ export class GitHubRepository extends Disposable { Logger.debug(`Fetch metadata - enter`, this.id); const { remote } = await this.ensure(); - this._metadata = this.getMetadataForRepo(remote.owner, remote.repositoryName); + this._metadata = this.getMetadataForRepo(remote.owner, remote.repositoryName).catch(e => { + if ((getErrorCode(e) === '404') && !isSamlError(e) && !this._isInaccessible) { + this._isInaccessible = true; + Logger.warn(`Repository ${remote.owner}/${remote.repositoryName} from remote ${remote.remoteName} in workspace folder ${this.rootUri.fsPath} returned HTTP 404 and will be skipped for this session.`, this.id); + } + throw e; + }); Logger.debug(`Fetch metadata ${remote.owner}/${remote.repositoryName} - done`, this.id); return this._metadata; } @@ -449,6 +457,9 @@ export class GitHubRepository extends Disposable { const { clone_url } = await this.getMetadata(); this.remote = GitHubRemote.remoteAsGitHub(parseRemote(this.remote.remoteName, clone_url, this.remote.gitProtocol)!, this.remote.githubServerType); } catch (e) { + if (this._isInaccessible) { + return false; + } Logger.warn(`Unable to resolve remote: ${e}`); if (isSamlError(e)) { return false; @@ -503,7 +514,9 @@ export class GitHubRepository extends Disposable { const data = await this.getMetadata(); return data.default_branch; } catch (e) { - Logger.warn(`Fetching default branch failed: ${e}`, this.id); + if (!this._isInaccessible) { + Logger.warn(`Fetching default branch for ${this.remote.owner}/${this.remote.repositoryName} in workspace folder ${this.rootUri.fsPath} failed: ${e}`, this.id); + } } return 'master'; diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index 6e4b1b9ef4..01a5e43331 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -53,6 +53,40 @@ describe('PullRequestManager', function () { sinon.restore(); }); + describe('updateRepositories', function () { + it('skips a repository after a 404 without affecting healthy repositories', async function () { + const inaccessibleUrl = 'https://github.com/owner/missing'; + const inaccessibleRemote = new GitHubRemote('origin', inaccessibleUrl, new Protocol(inaccessibleUrl), GitHubServerType.GitHubDotCom); + const inaccessibleRepository = new GitHubRepository(1, inaccessibleRemote, repository.rootUri, manager.credentialStore, telemetry, true); + const inaccessibleMetadata = sinon.stub(inaccessibleRepository as any, 'getMetadataForRepo').rejects(Object.assign(new Error('Not Found'), { status: 404 })); + const healthyUrl = 'https://github.com/owner/healthy'; + const healthyRemote = new GitHubRemote('upstream', healthyUrl, new Protocol(healthyUrl), GitHubServerType.GitHubDotCom); + const healthyRepository = new GitHubRepository(2, healthyRemote, repository.rootUri, manager.credentialStore, telemetry, true); + const healthyMetadata = sinon.stub(healthyRepository as any, 'getMetadataForRepo').resolves({ clone_url: healthyUrl } as never); + sinon.stub(manager.credentialStore, 'isAuthenticated').returns(true); + sinon.stub(manager.credentialStore, 'isAnyAuthenticated').returns(true); + sinon.stub(manager, 'computeAllGitHubRemotes').resolves([inaccessibleRemote, healthyRemote]); + sinon.stub(manager as any, 'createAndAddGitHubRepository').callsFake(async (remote: Remote) => remote.remoteName === 'origin' ? inaccessibleRepository : healthyRepository); + sinon.stub(manager as any, 'checkIfMissingUpstream').resolves(false as never); + sinon.stub(manager as any, 'associateLocalBranchesWithPRsOnFirstActivation').resolves(); + sinon.stub(manager, 'getAssignableUsers').resolves({}); + + await manager.updateRepositories(); + await manager.updateRepositories(); + + assert.deepStrictEqual(manager.gitHubRepositories, [healthyRepository]); + assert.strictEqual(inaccessibleMetadata.calledOnce, true); + assert.strictEqual(healthyMetadata.calledOnce, true); + assert.strictEqual((manager as any)._sessionIgnoredRemoteNames.has('origin'), true); + assert.strictEqual((manager as any)._inaccessibleRepos.has('owner/missing'), true); + assert.strictEqual((inaccessibleRepository as any)._isDisposed, true); + await assert.rejects( + manager.createGitHubRepository(inaccessibleRemote, manager.credentialStore), + /Repository owner\/missing is not accessible\./, + ); + }); + }); + describe('activePullRequest', function () { it('gets and sets the active pull request', function () { assert.strictEqual(manager.activePullRequest, undefined); diff --git a/src/test/github/githubRepository.test.ts b/src/test/github/githubRepository.test.ts index e83e87f550..946c2f7c54 100644 --- a/src/test/github/githubRepository.test.ts +++ b/src/test/github/githubRepository.test.ts @@ -17,6 +17,7 @@ import { GitHubManager } from '../../authentication/githubServer'; import { GitHubServerType } from '../../common/authentication'; import { CheckState, PullRequestCheckStatus } from '../../github/interface'; import { PullRequestBuilder as GraphQLPullRequestBuilder } from '../builders/graphql/pullRequestBuilder'; +import Logger from '../../common/logger'; describe('GitHubRepository', function () { let sinon: SinonSandbox; @@ -55,6 +56,45 @@ describe('GitHubRepository', function () { }); }); + describe('resolveRemote', function () { + beforeEach(function () { + sinon.stub(credentialStore, 'isAuthenticated').returns(true); + }); + + it('logs and caches an inaccessible repository after a 404', async function () { + const url = 'https://github.com/some/missing-repo'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const rootUri = Uri.file('/workspaces/missing-repo'); + const repo = new GitHubRepository(1, remote, rootUri, credentialStore, telemetry, true); + const metadata = sinon.stub(repo as any, 'getMetadataForRepo').rejects(Object.assign(new Error('Not Found'), { status: 404 })); + const warn = sinon.stub(Logger, 'warn'); + + assert.strictEqual(await repo.resolveRemote(), false); + assert.strictEqual(await repo.resolveRemote(), false); + + assert.strictEqual(repo.isInaccessible, true); + assert.strictEqual(metadata.calledOnce, true); + assert.strictEqual(warn.calledOnce, true); + assert.strictEqual( + warn.firstCall.args[0], + `Repository some/missing-repo from remote origin in workspace folder ${rootUri.fsPath} returned HTTP 404 and will be skipped for this session.`, + ); + }); + + it('does not cache a SAML 404 as inaccessible', async function () { + const url = 'https://github.com/some/saml-repo'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const repo = new GitHubRepository(1, remote, Uri.file('/workspaces/saml-repo'), credentialStore, telemetry, true); + sinon.stub(repo as any, 'getMetadataForRepo').rejects(Object.assign( + new Error('Resource protected by organization SAML enforcement.'), + { status: 404 }, + )); + + assert.strictEqual(await repo.resolveRemote(), false); + assert.strictEqual(repo.isInaccessible, false); + }); + }); + describe('deduplicateStatusChecks', function () { function createStatus(overrides: Partial & { id: string; context: string }): PullRequestCheckStatus { return { From dff7c5af7c90d30b7629990c898c486f6bb85bb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:02:41 +0000 Subject: [PATCH 3/4] Skip cached inaccessible repositories during updates Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 5 ++++- src/test/github/folderRepositoryManager.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 103a906ecb..7c60de352c 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -510,7 +510,10 @@ export class FolderRepositoryManager extends Disposable { const oldRepositories: GitHubRepository[] = []; this._githubRepositories.forEach(repo => oldRepositories.push(repo)); - const authenticatedRemotes = activeRemotes.filter(remote => this._credentialStore.isAuthenticated(remote.authProviderId)); + const authenticatedRemotes = activeRemotes.filter(remote => + this._credentialStore.isAuthenticated(remote.authProviderId) + && !this._inaccessibleRepos.has(`${remote.owner.toLowerCase()}/${remote.repositoryName.toLowerCase()}`) + ); for (const remote of authenticatedRemotes) { const repository = await this.createGitHubRepository(remote, this._credentialStore); resolveRemotePromises.push(repository.resolveRemote()); diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index 01a5e43331..61d3c2f244 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -65,7 +65,7 @@ describe('PullRequestManager', function () { const healthyMetadata = sinon.stub(healthyRepository as any, 'getMetadataForRepo').resolves({ clone_url: healthyUrl } as never); sinon.stub(manager.credentialStore, 'isAuthenticated').returns(true); sinon.stub(manager.credentialStore, 'isAnyAuthenticated').returns(true); - sinon.stub(manager, 'computeAllGitHubRemotes').resolves([inaccessibleRemote, healthyRemote]); + sinon.stub(manager as any, 'getActiveRemotes').resolves([inaccessibleRemote, healthyRemote] as never); sinon.stub(manager as any, 'createAndAddGitHubRepository').callsFake(async (remote: Remote) => remote.remoteName === 'origin' ? inaccessibleRepository : healthyRepository); sinon.stub(manager as any, 'checkIfMissingUpstream').resolves(false as never); sinon.stub(manager as any, 'associateLocalBranchesWithPRsOnFirstActivation').resolves(); From b63ce1cf706748d83b1405246588506c81284b60 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:24:09 +0200 Subject: [PATCH 4/4] Attestation commit