blob: 471313cd55bf1ac6721f9086f7ec6dce0dc20042 [file] [log] [blame]
// 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 'package:auto_submit/service/log.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 await 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 await 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 await 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 await github.issues.create(slug, issueRequest);
}
Future<Issue> getIssue({
required RepositorySlug slug,
required int issueNumber,
}) async {
return await github.issues.get(slug, issueNumber);
}
/// Fetches the specified pull request.
Future<PullRequest> getPullRequest(RepositorySlug slug, int pullRequestNumber) async {
return await github.pullRequests.get(slug, pullRequestNumber);
}
/// 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 await github.repositories.compareCommits(slug, refBase, refHead);
}
/// Removes a lable for a pull request.
Future<bool> removeLabel(RepositorySlug slug, int issueNumber, String label) async {
return await github.issues.removeLabelForIssue(slug, issueNumber, label);
}
/// Create a comment for a pull request.
Future<IssueComment> createComment(
RepositorySlug slug,
int issueNumber,
String body,
) async {
return await github.issues.createComment(slug, issueNumber, body);
}
/// 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;
}
/// 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 await 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);
}
}
/// Compare the filesets of the current pull request and the original pull
/// request that is being reverted.
Future<bool> comparePullRequests(RepositorySlug repositorySlug, PullRequest revert, PullRequest current) async {
final List<PullRequestFile> originalPullRequestFiles = await getPullRequestFiles(repositorySlug, revert);
final List<PullRequestFile> currentPullRequestFiles = await getPullRequestFiles(repositorySlug, current);
return validateFileSetsAreEqual(originalPullRequestFiles, currentPullRequestFiles);
}
/// Validate that each pull request has the same number of files and that the
/// file names match. This must be the case in order to process the revert.
bool validateFileSetsAreEqual(
List<PullRequestFile> revertRequestFileList,
List<PullRequestFile> originalRequestFileList,
) {
if (revertRequestFileList.length != originalRequestFileList.length) {
return false;
}
final List<String?> revertFileNames = [];
final List<String?> originalFileNames = [];
for (PullRequestFile element in revertRequestFileList) {
revertFileNames.add(element.filename);
}
for (PullRequestFile element in originalRequestFileList) {
originalFileNames.add(element.filename);
}
// At this point we know the file lists have the same amount of files but not the same files.
if (!revertFileNames.toSet().containsAll(originalFileNames) ||
!originalFileNames.toSet().containsAll(revertFileNames)) {
return false;
}
// At this point all the files are the same so we can iterate over one list to
// compare changes.
for (PullRequestFile revertRequestFile in revertRequestFileList) {
final PullRequestFile originalRequestFile =
originalRequestFileList.firstWhere((element) => element.filename == revertRequestFile.filename);
if (revertRequestFile.changesCount != originalRequestFile.changesCount ||
revertRequestFile.additionsCount != originalRequestFile.deletionsCount ||
revertRequestFile.deletionsCount != originalRequestFile.additionsCount) {
return false;
}
}
return true;
}
}