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
26 changes: 24 additions & 2 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -529,12 +532,27 @@ 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();
}
Comment thread
alexr00 marked this conversation as resolved.
if (missingSaml.length > 0) {
const result = await this._credentialStore.showSamlMessageAndAuth(missingSaml.map(repo => repo.remote.owner));
// Make a test call to see if the user has SAML enabled.
Expand Down Expand Up @@ -2955,6 +2973,10 @@ export class FolderRepositoryManager extends Disposable {

private _createGitHubRepositoryBulkhead = bulkhead(1, 300);
async createGitHubRepository(remote: Remote, credentialStore: CredentialStore, silent?: boolean, ignoreRemoteName: boolean = false): Promise<GitHubRepository> {
const repoKey = `${remote.owner.toLowerCase()}/${remote.repositoryName.toLowerCase()}`;
if (this._inaccessibleRepos.has(repoKey)) {
throw new Error(`Repository ${remote.owner}/${remote.repositoryName} is not accessible.`);
}
Comment thread
alexr00 marked this conversation as resolved.
// 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 }) ??
Expand Down
17 changes: 15 additions & 2 deletions src/github/githubRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export class GitHubRepository extends Disposable {
protected _initialized: boolean = false;
protected _hub: GitHub | undefined;
protected _metadata: Promise<IMetadata> | undefined;
private _isInaccessible: boolean = false;
public commentsController?: vscode.CommentController;
public commentsHandler?: PRCommentControllerRegistry;
private _pullRequestModelsByNumber: LRUCache<number, { model: PullRequestModel, disposables: vscode.Disposable[] }> = new LRUCache({
Expand All @@ -207,6 +208,7 @@ export class GitHubRepository extends Disposable {
private _maxItemNumberCache: { value: number; fetchedAt: number } | undefined;
private _maxItemNumberPromise: Promise<number | undefined> | undefined;
get areQueriesLimited(): boolean { return this._areQueriesLimited; }
get isInaccessible(): boolean { return this._isInaccessible; }

private _branchesCache: Map<string, string[]> = new Map();

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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';
Expand Down
34 changes: 34 additions & 0 deletions src/test/github/folderRepositoryManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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();
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);
Expand Down
40 changes: 40 additions & 0 deletions src/test/github/githubRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PullRequestCheckStatus> & { id: string; context: string }): PullRequestCheckStatus {
return {
Expand Down