blob: 3608997e1889885823b17ed98d8d675126ecbaf5 [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/datastore.dart';
import 'package:cocoon_service/src/service/github_service.dart';
import 'package:gcloud/db.dart';
import 'package:github/github.dart' show RepositoryCommit, RepositorySlug;
import 'package:github/hooks.dart';
import '../model/appengine/branch.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,
});
final Config config;
final GerritService gerritService;
/// Add a [CreateEvent] branch to Datastore.
Future<void> handleCreateRequest(CreateEvent createEvent) async {
log.info('the branch parsed from string request is ${createEvent.ref}');
final String? refType = createEvent.refType;
if (refType == 'tag') {
log.info('create branch event was rejected because it is a tag');
return;
}
final String? branch = createEvent.ref;
if (branch == null) {
log.warning('Branch is null, exiting early');
return;
}
final String repository = createEvent.repository!.slug().fullName;
final int lastActivity = createEvent.repository!.pushedAt!.millisecondsSinceEpoch;
final bool forked = createEvent.repository!.isFork;
if (forked) {
log.info('create branch event was rejected because the branch is a fork');
return;
}
final String id = '$repository/$branch';
log.info('the id used to create branch key was $id');
final DatastoreService datastore = DatastoreService.defaultProvider(config.db);
final Key<String> key = datastore.db.emptyKey.append<String>(Branch, id: id);
final Branch currentBranch = Branch(key: key, lastActivity: lastActivity);
try {
await datastore.lookupByValue<Branch>(currentBranch.key);
} on KeyNotFoundException {
log.info('create branch event was successful since the key is unique');
await datastore.insert(<Branch>[currentBranch]);
} catch (e) {
log.severe('Unexpected exception was encountered while inserting branch into database: $e');
}
}
/// Creates a flutter/recipes branch that aligns to a flutter/flutter branch.
///
/// Take the example repo history:
/// flutter/flutter: A -> B -> C -> D -> E
/// flutter/recipes: V -> W -> X -> Y -> Z
///
/// If flutter/flutter 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/flutter.
///
/// 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) async {
final RepositorySlug recipesSlug = RepositorySlug('flutter', 'recipes');
if ((await gerritService.branches('${recipesSlug.owner}-review.googlesource.com', recipesSlug.name,
subString: 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 GithubService githubService = await config.createDefaultGitHubService();
final List<RepositoryCommit> githubCommits = await githubService.listCommits(Config.flutterSlug, branch, null);
log.info('${Config.flutterSlug} branch commits: $githubCommits');
for (GerritCommit recipeCommit in recipeCommits) {
if (recipeCommit.author!.time!.isBefore(githubCommits.first.commit!.committer!.date!)) {
final String revision = recipeCommit.commit!;
return await gerritService.createBranch(recipesSlug, branch, revision);
}
}
throw InternalServerError('Failed to find a revision to branch Flutter recipes for $branch');
}
}