blob: dfa7f83832eeb0a07b9391a6d1e980014d06a7c1 [file] [log] [blame]
// Copyright 2014 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.
// @dart = 2.8
import 'dart:convert' show jsonEncode;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:fixnum/fixnum.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import './git.dart';
import './globals.dart';
import './proto/conductor_state.pb.dart' as pb;
import './proto/conductor_state.pbenum.dart' show ReleasePhase;
import './repository.dart';
import './state.dart';
import './stdio.dart';
const String kCandidateOption = 'candidate-branch';
const String kDartRevisionOption = 'dart-revision';
const String kEngineCherrypicksOption = 'engine-cherrypicks';
const String kEngineUpstreamOption = 'engine-upstream';
const String kFrameworkCherrypicksOption = 'framework-cherrypicks';
const String kFrameworkMirrorOption = 'framework-mirror';
const String kFrameworkUpstreamOption = 'framework-upstream';
const String kEngineMirrorOption = 'engine-mirror';
const String kReleaseOption = 'release-channel';
const String kStateOption = 'state-file';
/// Command to print the status of the current Flutter release.
class StartCommand extends Command<void> {
StartCommand({
@required this.checkouts,
@required this.flutterRoot,
}) : platform = checkouts.platform,
processManager = checkouts.processManager,
fileSystem = checkouts.fileSystem,
stdio = checkouts.stdio {
final String defaultPath = defaultStateFilePath(platform);
argParser.addOption(
kCandidateOption,
help: 'The candidate branch the release will be based on.',
);
argParser.addOption(
kReleaseOption,
help: 'The target release channel for the release.',
allowed: <String>['stable', 'beta', 'dev'],
);
argParser.addOption(
kFrameworkUpstreamOption,
defaultsTo: FrameworkRepository.defaultUpstream,
help:
'Configurable Framework repo upstream remote. Primarily for testing.',
hide: true,
);
argParser.addOption(
kEngineUpstreamOption,
defaultsTo: EngineRepository.defaultUpstream,
help: 'Configurable Engine repo upstream remote. Primarily for testing.',
hide: true,
);
argParser.addOption(
kFrameworkMirrorOption,
help: 'Framework repo mirror remote.',
);
argParser.addOption(
kEngineMirrorOption,
help: 'Engine repo mirror remote.',
);
argParser.addOption(
kStateOption,
defaultsTo: defaultPath,
help: 'Path to persistent state file. Defaults to $defaultPath',
);
argParser.addMultiOption(
kEngineCherrypicksOption,
help: 'Engine cherrypick hashes to be applied.',
defaultsTo: <String>[],
);
argParser.addMultiOption(
kFrameworkCherrypicksOption,
help: 'Framework cherrypick hashes to be applied.',
defaultsTo: <String>[],
);
argParser.addOption(
kDartRevisionOption,
help: 'New Dart revision to cherrypick.',
);
final Git git = Git(processManager);
conductorVersion = git.getOutput(
<String>['rev-parse', 'HEAD'],
'look up the current revision.',
workingDirectory: flutterRoot.path,
).trim();
assert(conductorVersion.isNotEmpty);
}
final Checkouts checkouts;
/// The root directory of the Flutter repository that houses the Conductor.
///
/// This directory is used to check the git revision of the Conductor.
final Directory flutterRoot;
final FileSystem fileSystem;
final Platform platform;
final ProcessManager processManager;
final Stdio stdio;
/// Git revision for the currently running Conductor.
String conductorVersion;
@override
String get name => 'start';
@override
String get description => 'Initialize a new Flutter release.';
@override
void run() {
if (!platform.isMacOS && !platform.isLinux) {
throw ConductorException(
'Error! This tool is only supported on macOS and Linux',
);
}
final File stateFile = checkouts.fileSystem.file(
getValueFromEnvOrArgs(kStateOption, argResults, platform.environment),
);
if (stateFile.existsSync()) {
throw ConductorException(
'Error! A persistent state file already found at ${argResults[kStateOption]}.\n\n'
'Run `conductor clean` to cancel a previous release.');
}
final String frameworkUpstream = getValueFromEnvOrArgs(
kFrameworkUpstreamOption,
argResults,
platform.environment,
);
final String frameworkMirror = getValueFromEnvOrArgs(
kFrameworkMirrorOption,
argResults,
platform.environment,
);
final String engineUpstream = getValueFromEnvOrArgs(
kEngineUpstreamOption,
argResults,
platform.environment,
);
final String engineMirror = getValueFromEnvOrArgs(
kEngineMirrorOption,
argResults,
platform.environment,
);
final String candidateBranch = getValueFromEnvOrArgs(
kCandidateOption,
argResults,
platform.environment,
);
final String releaseChannel = getValueFromEnvOrArgs(
kReleaseOption,
argResults,
platform.environment,
);
final List<String> frameworkCherrypickRevisions = getValuesFromEnvOrArgs(
kFrameworkCherrypicksOption,
argResults,
platform.environment,
);
final List<String> engineCherrypickRevisions = getValuesFromEnvOrArgs(
kEngineCherrypicksOption,
argResults,
platform.environment,
);
final String dartRevision = getValueFromEnvOrArgs(
kDartRevisionOption,
argResults,
platform.environment,
allowNull: true,
);
if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) {
throw ConductorException(
'Invalid release candidate branch "$candidateBranch". Text should '
'match the regex pattern /${releaseCandidateBranchRegex.pattern}/.',
);
}
final Int64 unixDate = Int64(DateTime.now().millisecondsSinceEpoch);
final pb.ConductorState state = pb.ConductorState();
state.releaseChannel = releaseChannel;
state.createdDate = unixDate;
state.lastUpdatedDate = unixDate;
final EngineRepository engine = EngineRepository(
checkouts,
initialRef: candidateBranch,
fetchRemote: Remote(
name: RemoteName.upstream,
url: engineUpstream,
),
pushRemote: Remote(
name: RemoteName.mirror,
url: engineMirror,
),
);
// Create a new branch so that we don't accidentally push to upstream
// candidateBranch.
engine.newBranch('cherrypicks-$candidateBranch');
if (dartRevision != null && dartRevision.isNotEmpty) {
engine.updateDartRevision(dartRevision);
engine.commit('Update Dart SDK to $dartRevision', addFirst: true);
}
final List<pb.Cherrypick> engineCherrypicks = _sortCherrypicks(
repository: engine,
cherrypicks: engineCherrypickRevisions,
upstreamRef: EngineRepository.defaultBranch,
releaseRef: candidateBranch,
).map((String revision) => pb.Cherrypick(
trunkRevision: revision,
state: pb.CherrypickState.PENDING,
)).toList();
for (final pb.Cherrypick cherrypick in engineCherrypicks) {
final String revision = cherrypick.trunkRevision;
final bool success = engine.canCherryPick(revision);
stdio.printTrace(
'Attempt to cherrypick $revision ${success ? 'succeeded' : 'failed'}',
);
if (success) {
engine.cherryPick(revision);
cherrypick.state = pb.CherrypickState.COMPLETED;
} else {
cherrypick.state = pb.CherrypickState.PENDING_WITH_CONFLICT;
}
}
final String engineHead = engine.reverseParse('HEAD');
state.engine = pb.Repository(
candidateBranch: candidateBranch,
startingGitHead: engineHead,
currentGitHead: engineHead,
checkoutPath: engine.checkoutDirectory.path,
cherrypicks: engineCherrypicks,
dartRevision: dartRevision,
);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
initialRef: candidateBranch,
fetchRemote: Remote(
name: RemoteName.upstream,
url: frameworkUpstream,
),
pushRemote: Remote(
name: RemoteName.mirror,
url: frameworkMirror,
),
);
framework.newBranch('cherrypicks-$candidateBranch');
final List<pb.Cherrypick> frameworkCherrypicks = _sortCherrypicks(
repository: framework,
cherrypicks: frameworkCherrypickRevisions,
upstreamRef: FrameworkRepository.defaultBranch,
releaseRef: candidateBranch,
).map((String revision) => pb.Cherrypick(
trunkRevision: revision,
state: pb.CherrypickState.PENDING,
)).toList();
for (final pb.Cherrypick cherrypick in frameworkCherrypicks) {
final String revision = cherrypick.trunkRevision;
final bool success = framework.canCherryPick(revision);
stdio.printTrace(
'Attempt to cherrypick $cherrypick ${success ? 'succeeded' : 'failed'}',
);
if (success) {
framework.cherryPick(revision);
cherrypick.state = pb.CherrypickState.COMPLETED;
} else {
cherrypick.state = pb.CherrypickState.PENDING_WITH_CONFLICT;
}
}
final String frameworkHead = framework.reverseParse('HEAD');
state.framework = pb.Repository(
candidateBranch: candidateBranch,
startingGitHead: frameworkHead,
currentGitHead: frameworkHead,
checkoutPath: framework.checkoutDirectory.path,
cherrypicks: frameworkCherrypicks,
);
state.lastPhase = ReleasePhase.INITIALIZE;
state.conductorVersion = conductorVersion;
stdio.printTrace('Writing state to file ${stateFile.path}...');
state.logs.addAll(stdio.logs);
stateFile.writeAsStringSync(
jsonEncode(state.toProto3Json()),
flush: true,
);
stdio.printStatus(presentState(state));
}
// To minimize merge conflicts, sort the commits by rev-list order.
List<String> _sortCherrypicks({
@required Repository repository,
@required List<String> cherrypicks,
@required String upstreamRef,
@required String releaseRef,
}) {
if (cherrypicks.isEmpty) {
return cherrypicks;
}
// Input cherrypick hashes that failed to be parsed by git.
final List<String> unknownCherrypicks = <String>[];
// Full 40-char hashes parsed by git.
final List<String> validatedCherrypicks = <String>[];
// Final, validated, sorted list of cherrypicks to be applied.
final List<String> sortedCherrypicks = <String>[];
for (final String cherrypick in cherrypicks) {
try {
final String fullRef = repository.reverseParse(cherrypick);
validatedCherrypicks.add(fullRef);
} on GitException {
// Catch this exception so that we can validate the rest.
unknownCherrypicks.add(cherrypick);
}
}
final String branchPoint = repository.branchPoint(
'${repository.fetchRemote.name}/$upstreamRef',
'${repository.fetchRemote.name}/$releaseRef',
);
// `git rev-list` returns newest first, so reverse this list
final List<String> upstreamRevlist = repository.revList(<String>[
'--ancestry-path',
'$branchPoint..$upstreamRef',
]).reversed.toList();
stdio.printStatus('upstreamRevList:\n${upstreamRevlist.join('\n')}\n');
stdio.printStatus('validatedCherrypicks:\n${validatedCherrypicks.join('\n')}\n');
for (final String upstreamRevision in upstreamRevlist) {
if (validatedCherrypicks.contains(upstreamRevision)) {
validatedCherrypicks.remove(upstreamRevision);
sortedCherrypicks.add(upstreamRevision);
if (unknownCherrypicks.isEmpty && validatedCherrypicks.isEmpty) {
return sortedCherrypicks;
}
}
}
// We were given input cherrypicks that were not present in the upstream
// rev-list
stdio.printError(
'The following ${repository.name} cherrypicks were not found in the '
'upstream $upstreamRef branch:',
);
for (final String cp in <String>[...validatedCherrypicks, ...unknownCherrypicks]) {
stdio.printError('\t$cp');
}
throw ConductorException(
'${validatedCherrypicks.length + unknownCherrypicks.length} unknown cherrypicks provided!',
);
}
}