blob: ca6d0247fd3903bae46bbc78561434f29101321e [file] [log] [blame] [edit]
// Copyright 2022 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:cocoon_server/logging.dart';
import 'package:github/github.dart';
/// If a pull request was behind the tip of tree by _kBehindToT commits
/// then the bot tries to rebase it
const int _kBehindToT = 10;
/// [GithubService] handles communication with the GitHub API.
class GithubService {
GithubService(this.github);
final GitHub github;
/// Retrieves check runs with the ref.
Future<List<CheckRun>> getCheckRuns(
RepositorySlug slug,
String ref,
) async {
return github.checks.checkRuns.listCheckRunsForRef(slug, ref: ref).toList();
}
Future<List<CheckRun>> getCheckRunsFiltered({
required RepositorySlug slug,
required String ref,
String? checkName,
CheckRunStatus? status,
CheckRunFilter? filter,
}) async {
return github.checks.checkRuns
.listCheckRunsForRef(
slug,
ref: ref,
checkName: checkName,
status: status,
filter: filter,
)
.toList();
}
/// Fetches the specified commit.
Future<RepositoryCommit> getCommit(
RepositorySlug slug,
String sha,
) async {
return github.repositories.getCommit(slug, sha);
}
Future<List<PullRequestFile>> getPullRequestFiles(
RepositorySlug slug,
PullRequest pullRequest,
) async {
final int? pullRequestId = pullRequest.number;
final List<PullRequestFile> listPullRequestFiles = [];
if (pullRequestId == null) {
return listPullRequestFiles;
}
final Stream<PullRequestFile> pullRequestFiles = github.pullRequests.listFiles(slug, pullRequestId);
await for (PullRequestFile file in pullRequestFiles) {
listPullRequestFiles.add(file);
}
return listPullRequestFiles;
}
/// Create a new issue in github.
Future<Issue> createIssue({
required RepositorySlug slug,
required String title,
required String body,
List<String>? labels,
String? assignee,
List<String>? assignees,
String? state,
}) async {
final IssueRequest issueRequest = IssueRequest(
title: title,
body: body,
labels: labels,
assignee: assignee,
assignees: assignees,
state: state,
);
return github.issues.create(slug, issueRequest);
}
Future<Issue> getIssue({
required RepositorySlug slug,
required int issueNumber,
}) async {
return github.issues.get(slug, issueNumber);
}
/// Create a pull request.
Future<PullRequest> createPullRequest({
required RepositorySlug slug,
String? title,
String? head,
required String base,
bool draft = false,
String? body,
}) async {
final CreatePullRequest createPullRequest = CreatePullRequest(
title,
head,
base,
draft: draft,
body: body,
);
return github.pullRequests.create(slug, createPullRequest);
}
/// Fetches the specified pull request.
Future<PullRequest> getPullRequest(
RepositorySlug slug,
int pullRequestNumber,
) async {
return github.pullRequests.get(slug, pullRequestNumber);
}
Future<List<PullRequest>> listPullRequests(
RepositorySlug slug, {
int? pages,
String? base,
String direction = 'desc',
String? head,
String sort = 'created',
String state = 'open',
}) async {
final List<PullRequest> pullRequestsFound = [];
final Stream<PullRequest> pullRequestStream = github.pullRequests.list(
slug,
pages: pages,
direction: direction,
head: head,
sort: sort,
state: state,
);
await for (PullRequest pullRequest in pullRequestStream) {
pullRequestsFound.add(pullRequest);
}
return pullRequestsFound;
}
Future<bool> addReviewersToPullRequest(
RepositorySlug slug,
int pullRequestNumber,
List<String> reviewerLogins,
) async {
final response = await github.request(
'POST',
'/repos/${slug.fullName}/pulls/$pullRequestNumber/requested_reviewers',
body: GitHubJson.encode({'reviewers': reviewerLogins}),
);
return response.statusCode == StatusCodes.CREATED;
}
/// Get the reviews for Pull Request with number pullRequestNumber.
Future<List<PullRequestReview>> getPullRequestReviews(
RepositorySlug slug,
int pullRequestNumber,
) async {
return github.pullRequests.listReviews(slug, pullRequestNumber).toList();
}
/// Compares two commits to fetch diff.
///
/// The response will include details on the files that were changed between the two commits.
/// Relevant APIs: https://docs.github.com/en/rest/reference/commits#compare-two-commits
Future<GitHubComparison> compareTwoCommits(
RepositorySlug slug,
String refBase,
String refHead,
) async {
return github.repositories.compareCommits(slug, refBase, refHead);
}
/// Removes a label from a pull request.
Future<bool> removeLabel(
RepositorySlug slug,
int issueNumber,
String label,
) async {
return github.issues.removeLabelForIssue(slug, issueNumber, label);
}
/// Add labels to a pull request.
Future<List<IssueLabel>> addLabels(
RepositorySlug slug,
int issueNumber,
List<String> labels,
) async {
return github.issues.addLabelsToIssue(slug, issueNumber, labels);
}
/// Relevant API: https://docs.github.com/en/rest/issues/assignees?apiVersion=2022-11-28#add-assignees-to-an-issue
Future<bool> addAssignee(
RepositorySlug slug,
int number,
List<String> assignees,
) async {
final response = await github.request(
'POST',
'/repos/${slug.fullName}/issues/$number/assignees',
body: GitHubJson.encode({'assignees': assignees}),
);
return response.statusCode == StatusCodes.CREATED;
}
/// Create a comment for a pull request.
Future<IssueComment> createComment(
RepositorySlug slug,
int issueNumber,
String body,
) async {
return github.issues.createComment(slug, issueNumber, body);
}
Future<List<IssueComment>> getIssueComments(
RepositorySlug slug,
int issueNumber,
) async {
return github.issues.listCommentsByIssue(slug, issueNumber).toList();
}
/// Update a pull request branch
Future<bool> updateBranch(
RepositorySlug slug,
int number,
String headSha,
) async {
final response = await github.request(
'PUT',
'/repos/${slug.fullName}/pulls/$number/update-branch',
body: GitHubJson.encode({'expected_head_sha': headSha}),
);
return response.statusCode == StatusCodes.ACCEPTED;
}
Future<Branch> getBranch(
RepositorySlug slug,
String branchName,
) async {
return github.repositories.getBranch(slug, branchName);
}
Future<bool> deleteBranch(
RepositorySlug slug,
String branchName,
) async {
final String ref = 'heads/$branchName';
return github.git.deleteReference(slug, ref);
}
/// Merges a pull request according to the MergeMethod type. Current supported
/// merge method types are merge, rebase and squash.
Future<PullRequestMerge> mergePullRequest(
RepositorySlug slug,
int number, {
String? commitMessage,
MergeMethod mergeMethod = MergeMethod.merge,
String? requestSha,
}) async {
return github.pullRequests.merge(
slug,
number,
message: commitMessage,
mergeMethod: mergeMethod,
requestSha: requestSha,
);
}
/// Automerges a given pull request with HEAD to ensure the commit is not in conflicting state.
Future<void> autoMergeBranch(PullRequest pullRequest) async {
final RepositorySlug slug = pullRequest.base!.repo!.slug();
final int prNumber = pullRequest.number!;
final RepositoryCommit totCommit = await getCommit(slug, 'HEAD');
final GitHubComparison comparison = await compareTwoCommits(slug, totCommit.sha!, pullRequest.base!.sha!);
if (comparison.behindBy! >= _kBehindToT) {
log.info('The current branch is behind by ${comparison.behindBy} commits.');
final String headSha = pullRequest.head!.sha!;
await updateBranch(slug, prNumber, headSha);
}
}
/// Get contents from a repository at the supplied path.
Future<String> getFileContents(RepositorySlug slug, String path, {String? ref}) async {
final RepositoryContents repositoryContents = await github.repositories.getContents(slug, path, ref: ref);
if (!repositoryContents.isFile) {
throw 'Contents do not point to a file.';
}
final String content = utf8.decode(base64.decode(repositoryContents.file!.content!.replaceAll('\n', '')));
return content;
}
/// Check to see if user is a member of team in org.
///
/// Note that we catch here as the api returns a 404 if the user has no
/// membership in general or is not a member of the team.
Future<bool> isTeamMember(String team, String user, String org) async {
try {
final TeamMembershipState teamMembershipState =
await github.organizations.getTeamMembershipByName(org, team, user);
return teamMembershipState.isActive;
} on GitHubError {
return false;
}
}
/// Get the definition of a single repository
Future<Repository> getRepository(RepositorySlug slug) async {
return github.repositories.getRepository(slug);
}
Future<String> getDefaultBranch(RepositorySlug slug) async {
final Repository repository = await getRepository(slug);
return repository.defaultBranch;
}
}