blob: a6072d91412d19f4bc82664e941eabee99ee106f [file] [log] [blame]
// Copyright 2021 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:cocoon_service/src/service/config.dart';
import 'package:cocoon_service/src/service/github_service.dart';
import 'package:github/github.dart' as gh;
import 'package:retry/retry.dart';
import '../model/gerrit/commit.dart';
import '../request_handling/exceptions.dart';
import 'gerrit_service.dart';
import 'logging.dart';
class RetryException implements Exception {}
/// A class to manage GitHub branches.
///
/// Track branch activities such as branch creation, and helps manage release branches.
class BranchService {
BranchService({
required this.config,
required this.gerritService,
this.retryOptions = const RetryOptions(maxAttempts: 3),
});
final Config config;
final GerritService gerritService;
final RetryOptions retryOptions;
/// Creates a flutter/recipes branch that aligns to a flutter/engine commit.
///
/// Take the example repo history:
/// flutter/engine: A -> B -> C -> D -> E
/// flutter/recipes: V -> W -> X -> Y -> Z
///
/// If flutter/engine branches at C, this finds the flutter/recipes commit that should be used for C.
/// The best guess for a flutter/recipes commit that aligns with C is whatever was the most recently committed
/// before C was committed.
///
/// Once the flutter/recipes commit is found, it is branched to match flutter/engine.
///
/// Generally, this should work. However, some edge cases may require CPs. Such as when commits land in a
/// short timespan, and require the release manager to CP onto the recipes branch (in the case of reverts).
Future<void> branchFlutterRecipes(String branch, String engineSha) async {
final gh.RepositorySlug recipesSlug = gh.RepositorySlug('flutter', 'recipes');
if ((await gerritService.branches(
'${recipesSlug.owner}-review.googlesource.com',
recipesSlug.name,
filterRegex: branch,
))
.contains(branch)) {
// subString is a regex, and can return multiple matches
log.warning('$branch already exists for $recipesSlug');
throw BadRequestException('$branch already exists');
}
final Iterable<GerritCommit> recipeCommits =
await gerritService.commits(recipesSlug, Config.defaultBranch(recipesSlug));
log.info('$recipesSlug commits: $recipeCommits');
final gh.RepositoryCommit engineCommit = await retryOptions.retry(
() async {
final GithubService githubService = await config.createDefaultGitHubService();
return githubService.github.repositories.getCommit(Config.engineSlug, engineSha);
},
retryIf: (Exception e) => e is gh.GitHubError,
);
log.info('${Config.engineSlug} commit: $engineCommit');
final DateTime? branchTime = engineCommit.commit?.committer?.date;
if (branchTime == null) {
throw BadRequestException('$engineSha has no commit time');
}
log.info('Searching for a recipe commit before $branchTime');
for (GerritCommit recipeCommit in recipeCommits) {
final DateTime? recipeTime = recipeCommit.author?.time;
if (recipeTime != null && recipeTime.isBefore(branchTime)) {
final String revision = recipeCommit.commit!;
return gerritService.createBranch(recipesSlug, branch, revision);
}
}
throw InternalServerError('Failed to find a revision to flutter/recipes for $branch before $branchTime');
}
/// Returns a Map that contains the latest google3 roll, beta, and stable branches.
///
/// Latest beta and stable branches are retrieved based on 'beta' and 'stable' tags. Dev branch is retrived
/// as the latest flutter candidate branch.
Future<List<Map<String, String>>> getReleaseBranches({
required GithubService githubService,
required gh.RepositorySlug slug,
}) async {
final List<gh.Branch> branches = await githubService.github.repositories.listBranches(slug).toList();
final String latestCandidateBranch = await _getLatestCandidateBranch(
github: githubService.github,
slug: slug,
branches: branches,
);
final String betaName = await _getBranchNameFromFile(
githubService: githubService,
slug: slug,
branchName: 'beta',
);
final String stableName = await _getBranchNameFromFile(
githubService: githubService,
slug: slug,
branchName: 'stable',
);
return <Map<String, String>>[
{
'branch': Config.defaultBranch(slug),
'name': 'HEAD',
},
{
'branch': stableName,
'name': 'stable',
},
{
'branch': betaName,
'name': 'beta',
},
{
'branch': latestCandidateBranch,
'name': 'dev',
},
];
}
Future<String> _getBranchNameFromFile({
required GithubService githubService,
required gh.RepositorySlug slug,
required String branchName,
}) async {
return (await githubService.getFileContent(
slug,
'bin/internal/release-candidate-branch.version',
ref: branchName,
))
.trim();
}
/// Retrieve the latest canidate branch from all candidate branches.
Future<String> _getLatestCandidateBranch({
required gh.GitHub github,
required gh.RepositorySlug slug,
required List<gh.Branch> branches,
}) async {
final RegExp candidateBranchName = RegExp(r'^flutter-\d+\.\d+-candidate\.\d+$');
final List<gh.Branch> devBranches = branches.where((gh.Branch b) => candidateBranchName.hasMatch(b.name!)).toList();
devBranches.sort((b, a) => (_versionSum(a.name!)).compareTo(_versionSum(b.name!)));
final String devBranchName = devBranches.take(1).single.name!;
return devBranchName;
}
/// Helper function to convert candidate branch versions to numbers for comparison.
int _versionSum(String tagOrBranchName) {
final List<String> digits = tagOrBranchName.replaceAll(r'flutter|candidate', '0').split(RegExp(r'\.|\-'));
int versionSum = 0;
for (String digit in digits) {
final int? d = int.tryParse(digit);
if (d == null) {
continue;
}
versionSum = versionSum * 100 + d;
}
return versionSum;
}
}