blob: 6cec27bbafe71c5fad19b1ca4e9ef5683a509d2f [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();
}
/// 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;
}
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 {
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;
}
/// 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 behinds 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 {
List<PullRequestFile> originalPullRequestFiles = await getPullRequestFiles(repositorySlug, revert);
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> revertPullRequestFiles,
List<PullRequestFile> currentPullRequestFiles,
) {
List<String?> revertFileNames = [];
List<String?> currentFileNames = [];
for (PullRequestFile element in revertPullRequestFiles) {
revertFileNames.add(element.filename);
}
for (PullRequestFile element in currentPullRequestFiles) {
currentFileNames.add(element.filename);
}
return revertFileNames.toSet().containsAll(currentFileNames) &&
currentFileNames.toSet().containsAll(revertFileNames);
}
}