Skip to content

Commit

Permalink
Add Team Reviewers to Pull Request
Browse files Browse the repository at this point in the history
Fixes #1126
  • Loading branch information
alexr00 committed Feb 10, 2023
1 parent a648ee9 commit 3acde33
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 64 deletions.
2 changes: 1 addition & 1 deletion src/common/githubRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Protocol } from './protocol';
export class GitHubRef {
public repositoryCloneUrl: Protocol;
constructor(public ref: string, public label: string, public sha: string, repositoryCloneUrl: string,
public readonly owner: string, public readonly name: string) {
public readonly owner: string, public readonly name: string, public readonly isInOrganization: boolean) {
this.repositoryCloneUrl = new Protocol(repositoryCloneUrl);
}
}
4 changes: 2 additions & 2 deletions src/github/activityBarViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { dispose, formatError } from '../common/utils';
import { getNonce, IRequestMessage, WebviewViewBase } from '../common/webview';
import { ReviewManager } from '../view/reviewManager';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { GithubItemStateEnum, ReviewEvent, ReviewState } from './interface';
import { GithubItemStateEnum, reviewerId, ReviewEvent, ReviewState } from './interface';
import { PullRequestModel } from './pullRequestModel';
import { getDefaultMergeMethod } from './pullRequestOverview';
import { PullRequestView } from './pullRequestOverviewCommon';
Expand Down Expand Up @@ -252,7 +252,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W
private updateReviewers(review?: CommonReviewEvent): void {
if (review) {
const existingReviewer = this._existingReviewers.find(
reviewer => review.user.login === reviewer.reviewer.login,
reviewer => review.user.login === reviewerId(reviewer.reviewer),
);
if (existingReviewer) {
existingReviewer.state = review.state;
Expand Down
2 changes: 1 addition & 1 deletion src/github/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const PROMPT_FOR_SIGN_IN_STORAGE_KEY = 'login';

// If the scopes are changed, make sure to notify all interested parties to make sure this won't cause problems.
const SCOPES_OLD = ['read:user', 'user:email', 'repo'];
export const SCOPES = ['read:user', 'user:email', 'repo', 'workflow'];
export const SCOPES = ['read:user', 'user:email', 'repo', 'workflow', 'read:org'];

export interface GitHub {
octokit: LoggingOctokit;
Expand Down
89 changes: 87 additions & 2 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { OctokitCommon } from './common';
import { CredentialStore } from './credentials';
import { GitHubRepository, ItemsData, PullRequestData, ViewerPermission } from './githubRepository';
import { PullRequestState, UserResponse } from './graphql';
import { IAccount, ILabel, IMilestone, IPullRequestsPagingOptions, PRType, RepoAccessAndMergeMethods, User } from './interface';
import { IAccount, ILabel, IMilestone, IPullRequestsPagingOptions, ITeam, PRType, RepoAccessAndMergeMethods, User } from './interface';
import { IssueModel } from './issueModel';
import { MilestoneModel } from './milestoneModel';
import { PullRequestGitHelper, PullRequestMetadata } from './pullRequestGitHelper';
Expand All @@ -39,6 +39,7 @@ import {
getRelatedUsersFromTimelineEvents,
loginComparator,
parseGraphQLUser,
teamComparator,
variableSubstitution,
} from './utils';

Expand Down Expand Up @@ -125,7 +126,9 @@ export class FolderRepositoryManager implements vscode.Disposable {
private _mentionableUsers?: { [key: string]: IAccount[] };
private _fetchMentionableUsersPromise?: Promise<{ [key: string]: IAccount[] }>;
private _assignableUsers?: { [key: string]: IAccount[] };
private _teamReviewers?: { [key: string]: ITeam[] };
private _fetchAssignableUsersPromise?: Promise<{ [key: string]: IAccount[] }>;
private _fetchTeamReviewersPromise?: Promise<{ [key: string]: ITeam[] }>;
private _gitBlameCache: { [key: string]: string } = {};
private _githubManager: GitHubManager;
private _repositoryPageInformation: Map<string, PageInformation> = new Map<string, PageInformation>();
Expand Down Expand Up @@ -731,7 +734,7 @@ export class FolderRepositoryManager implements vscode.Disposable {
// file doesn't exist
}
if (repoSpecificCache && repoSpecificCache.toString()) {
cache[repo.remote.repositoryName] = JSON.parse(repoSpecificCache.toString()) ?? [];
cache[repo.remote.remoteName] = JSON.parse(repoSpecificCache.toString()) ?? [];
return true;
}
}))).every(value => value);
Expand All @@ -744,6 +747,44 @@ export class FolderRepositoryManager implements vscode.Disposable {
return undefined;
}

private async getTeamReviewersFromGlobalState(): Promise<{ [key: string]: ITeam[] } | undefined> {
Logger.appendLine('Trying to use globalState for team reviewers.');

const teamReviewersCacheLocation = vscode.Uri.joinPath(this.context.globalStorageUri, 'teamReviewers');
let teamReviewersCacheExists;
try {
teamReviewersCacheExists = await vscode.workspace.fs.stat(teamReviewersCacheLocation);
} catch (e) {
// file doesn't exit
}
if (!teamReviewersCacheExists) {
return undefined;
}

const cache: { [key: string]: ITeam[] } = {};
const hasAllRepos = (await Promise.all(this._githubRepositories.map(async (repo) => {
const key = `${repo.remote.owner}/${repo.remote.repositoryName}.json`;
const repoSpecificFile = vscode.Uri.joinPath(teamReviewersCacheLocation, key);
let repoSpecificCache;
try {
repoSpecificCache = await vscode.workspace.fs.readFile(repoSpecificFile);
} catch (e) {
// file doesn't exist
}
if (repoSpecificCache && repoSpecificCache.toString()) {
cache[repo.remote.remoteName] = JSON.parse(repoSpecificCache.toString()) ?? [];
return true;
}
}))).every(value => value);
if (hasAllRepos) {
Logger.appendLine(`Using globalState team reviewers for ${Object.keys(cache).length}.`);
return cache;
}

Logger.appendLine(`No globalState for team reviewers.`);
return undefined;
}

private createFetchMentionableUsersPromise(): Promise<{ [key: string]: IAccount[] }> {
const cache: { [key: string]: IAccount[] } = {};
return new Promise<{ [key: string]: IAccount[] }>(resolve => {
Expand Down Expand Up @@ -818,6 +859,50 @@ export class FolderRepositoryManager implements vscode.Disposable {
return this._fetchAssignableUsersPromise;
}

async getTeamReviewers(clearCache?: boolean): Promise<{ [key: string]: ITeam[] }> {
if (clearCache) {
delete this._teamReviewers;
}

if (this._teamReviewers) {
return this._teamReviewers;
}

const globalStateTeamReviewers = clearCache ? undefined : await this.getTeamReviewersFromGlobalState();
if (globalStateTeamReviewers) {
return globalStateTeamReviewers || {};
}

if (!this._fetchTeamReviewersPromise) {
const cache: { [key: string]: ITeam[] } = {};
return (this._fetchTeamReviewersPromise = new Promise(resolve => {
const promises = this._githubRepositories.map(async githubRepository => {
const data = await githubRepository.getTeams();
cache[githubRepository.remote.remoteName] = data.sort(teamComparator);
return;
});

Promise.all(promises).then(() => {
this._teamReviewers = cache;
this._fetchTeamReviewersPromise = undefined;
const teamReviewersCacheLocation = vscode.Uri.joinPath(this.context.globalStorageUri, 'teamReviewers');
Promise.all(this._githubRepositories.map(async (repo) => {
const key = `${repo.remote.owner}/${repo.remote.repositoryName}.json`;
const repoSpecificFile = vscode.Uri.joinPath(teamReviewersCacheLocation, key);
await vscode.workspace.fs.writeFile(repoSpecificFile, new TextEncoder().encode(JSON.stringify(cache[repo.remote.remoteName])));
}));
resolve(cache);
});
}));
}

return this._fetchTeamReviewersPromise;
}

async getOrgTeamsCount(repository: GitHubRepository): Promise<number> {
return repository.getOrgTeamsCount();
}

async getPullRequestParticipants(githubRepository: GitHubRepository, pullRequestNumber: number): Promise<{ participants: IAccount[], viewer: IAccount }> {
return {
participants: await githubRepository.getPullRequestParticipants(pullRequestNumber),
Expand Down
83 changes: 82 additions & 1 deletion src/github/githubRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import {
MaxIssueResponse,
MentionableUsersResponse,
MilestoneIssuesResponse,
OrganizationTeamsCountResponse,
OrganizationTeamsResponse,
PullRequestParticipantsResponse,
PullRequestResponse,
PullRequestsResponse,
ViewerPermissionResponse,
} from './graphql';
import { CheckState, IAccount, IMilestone, Issue, PullRequest, PullRequestChecks, RepoAccessAndMergeMethods } from './interface';
import { CheckState, IAccount, IMilestone, Issue, ITeam, PullRequest, PullRequestChecks, RepoAccessAndMergeMethods } from './interface';
import { IssueModel } from './issueModel';
import { LoggingOctokit } from './loggingOctokit';
import { PullRequestModel } from './pullRequestModel';
Expand Down Expand Up @@ -985,6 +987,85 @@ export class GitHubRepository implements vscode.Disposable {
return ret;
}

async getOrgTeamsCount(): Promise<number> {
Logger.debug(`Fetch Teams Count - enter`, GitHubRepository.ID);
const { query, remote, schema } = await this.ensure();

try {
const result: { data: OrganizationTeamsCountResponse } = await query<OrganizationTeamsCountResponse>({
query: schema.GetOrganizationTeamsCount,
variables: {
login: remote.owner
},
});
return result.data.organization.teams.totalCount;
} catch (e) {
Logger.debug(`Unable to fetch teams Count: ${e}`, GitHubRepository.ID);
if (
e.graphQLErrors &&
e.graphQLErrors.length > 0 &&
e.graphQLErrors[0].type === 'INSUFFICIENT_SCOPES'
) {
vscode.window.showWarningMessage(
`GitHub teams features will not work. ${e.graphQLErrors[0].message}`,
);
}
return 0;
}
}

async getTeams(): Promise<ITeam[]> {
Logger.debug(`Fetch Teams - enter`, GitHubRepository.ID);
const { query, remote, schema } = await this.ensure();

let after: string | null = null;
let hasNextPage = false;
const ret: ITeam[] = [];

do {
try {
const result: { data: OrganizationTeamsResponse } = await query<OrganizationTeamsResponse>({
query: schema.GetOrganizationTeams,
variables: {
login: remote.owner,
after: after,
repoName: remote.repositoryName,
},
});

result.data.organization.teams.nodes.forEach(node => {
if (node.repositories.nodes.find(repo => repo.name === remote.repositoryName)) {
ret.push({
avatarUrl: getAvatarWithEnterpriseFallback(node.avatarUrl, undefined, this.remote.authProviderId),
name: node.name,
url: node.url,
slug: node.slug,
id: node.id,
org: remote.owner
});
}
});

hasNextPage = result.data.organization.teams.pageInfo.hasNextPage;
after = result.data.organization.teams.pageInfo.endCursor;
} catch (e) {
Logger.debug(`Unable to fetch teams: ${e}`, GitHubRepository.ID);
if (
e.graphQLErrors &&
e.graphQLErrors.length > 0 &&
e.graphQLErrors[0].type === 'INSUFFICIENT_SCOPES'
) {
vscode.window.showWarningMessage(
`GitHub teams features will not work. ${e.graphQLErrors[0].message}`,
);
}
return ret;
}
} while (hasNextPage);

return ret;
}

async getPullRequestParticipants(pullRequestNumber: number): Promise<IAccount[]> {
Logger.debug(`Fetch participants from a Pull Request`, GitHubRepository.ID);
const { query, remote, schema } = await this.ensure();
Expand Down
59 changes: 59 additions & 0 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ export interface Account {
email: string;
}

interface Team {
avatarUrl: string;
name: string;
url: string;
repositories: {
nodes: {
name: string
}[];
};
slug: string;
id: string;
}

export interface ReviewComment {
__typename: string;
id: string;
Expand Down Expand Up @@ -225,6 +238,29 @@ export interface PendingReviewIdResponse {
rateLimit: RateLimit;
}

export interface GetReviewRequestsResponse {
repository: {
pullRequest: {
reviewRequests: {
nodes: {
requestedReviewer: {
// Shared properties between accounts and teams
avatarUrl: string;
url: string;
name: string;
// Account properties
login?: string;
email?: string;
// Team properties
slug?: string;
id?: string;
};
}[];
};
};
};
};

export interface PullRequestState {
repository: {
pullRequest: {
Expand Down Expand Up @@ -271,6 +307,28 @@ export interface AssignableUsersResponse {
rateLimit: RateLimit;
}

export interface OrganizationTeamsCountResponse {
organization: {
teams: {
totalCount: number;
};
};
}

export interface OrganizationTeamsResponse {
organization: {
teams: {
nodes: Team[];
totalCount: number;
pageInfo: {
hasNextPage: boolean;
endCursor: string;
};
};
};
rateLimit: RateLimit;
}

export interface PullRequestParticipantsResponse {
repository: {
pullRequest: {
Expand Down Expand Up @@ -398,6 +456,7 @@ export interface ListBranchesResponse {
}

export interface RefRepository {
isInOrganization: boolean;
owner: {
login: string;
};
Expand Down
Loading

0 comments on commit 3acde33

Please sign in to comment.