blob: f54add1fa13c0cf42e1cbb14c65c37c2c876ca01 [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.
import 'dart:convert' show jsonDecode;
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:platform/platform.dart';
import './git.dart';
import './globals.dart' as globals;
import './stdio.dart';
import './version.dart';
/// A source code repository.
class Repository {
Repository({
@required this.name,
@required this.upstream,
@required this.processManager,
@required this.stdio,
@required this.platform,
@required this.fileSystem,
@required this.parentDirectory,
this.localUpstream = false,
this.useExistingCheckout = false,
}) : git = Git(processManager),
assert(localUpstream != null),
assert(useExistingCheckout != null);
final String name;
final String upstream;
final Git git;
final ProcessManager processManager;
final Stdio stdio;
final Platform platform;
final FileSystem fileSystem;
final Directory parentDirectory;
final bool useExistingCheckout;
/// If the repository will be used as an upstream for a test repo.
final bool localUpstream;
Directory _checkoutDirectory;
/// Lazily-loaded directory for the repository checkout.
///
/// Cloning a repository is time-consuming, thus the repository is not cloned
/// until this getter is called.
Directory get checkoutDirectory {
if (_checkoutDirectory != null) {
return _checkoutDirectory;
}
_checkoutDirectory = parentDirectory.childDirectory(name);
if (checkoutDirectory.existsSync() && !useExistingCheckout) {
deleteDirectory();
}
if (!checkoutDirectory.existsSync()) {
stdio.printTrace('Cloning $name to ${checkoutDirectory.path}...');
git.run(
<String>['clone', '--', upstream, checkoutDirectory.path],
'Cloning $name repo',
workingDirectory: parentDirectory.path,
);
if (localUpstream) {
// These branches must exist locally for the repo that depends on it
// to fetch and push to.
for (final String channel in globals.kReleaseChannels) {
git.run(
<String>['checkout', channel, '--'],
'check out branch $channel locally',
workingDirectory: checkoutDirectory.path,
);
}
}
} else {
stdio.printTrace(
'Using existing $name repo at ${checkoutDirectory.path}...',
);
}
return _checkoutDirectory;
}
void deleteDirectory() {
if (!checkoutDirectory.existsSync()) {
stdio.printTrace(
'Tried to delete ${checkoutDirectory.path} but it does not exist.',
);
return;
}
stdio.printTrace('Deleting $name from ${checkoutDirectory.path}...');
checkoutDirectory.deleteSync(recursive: true);
}
/// The URL of the remote named [remoteName].
String remoteUrl(String remoteName) {
assert(remoteName != null);
return git.getOutput(
<String>['remote', 'get-url', remoteName],
'verify the URL of the $remoteName remote',
workingDirectory: checkoutDirectory.path,
);
}
/// Verify the repository's git checkout is clean.
bool gitCheckoutClean() {
final String output = git.getOutput(
<String>['status', '--porcelain'],
'check that the git checkout is clean',
workingDirectory: checkoutDirectory.path,
);
return output == '';
}
/// Fetch all branches and associated commits and tags from [remoteName].
void fetch(String remoteName) {
git.run(
<String>['fetch', remoteName, '--tags'],
'fetch $remoteName --tags',
workingDirectory: checkoutDirectory.path,
);
}
/// Obtain the version tag of the previous dev release.
String getFullTag(String remoteName) {
const String glob = '*.*.*-*.*.pre';
// describe the latest dev release
final String ref = 'refs/remotes/$remoteName/dev';
return git.getOutput(
<String>['describe', '--match', glob, '--exact-match', '--tags', ref],
'obtain last released version number',
workingDirectory: checkoutDirectory.path,
);
}
/// Look up the commit for [ref].
String reverseParse(String ref) {
final String revisionHash = git.getOutput(
<String>['rev-parse', ref],
'look up the commit for the ref $ref',
workingDirectory: checkoutDirectory.path,
);
assert(revisionHash.isNotEmpty);
return revisionHash;
}
/// Determines if one ref is an ancestor for another.
bool isAncestor(String possibleAncestor, String possibleDescendant) {
final int exitcode = git.run(
<String>[
'merge-base',
'--is-ancestor',
possibleDescendant,
possibleAncestor
],
'verify $possibleAncestor is a direct ancestor of $possibleDescendant.',
allowNonZeroExitCode: true,
workingDirectory: checkoutDirectory.path,
);
return exitcode == 0;
}
/// Determines if a given commit has a tag.
bool isCommitTagged(String commit) {
final int exitcode = git.run(
<String>['describe', '--exact-match', '--tags', commit],
'verify $commit is already tagged',
allowNonZeroExitCode: true,
workingDirectory: checkoutDirectory.path,
);
return exitcode == 0;
}
/// Resets repository HEAD to [commit].
void reset(String commit) {
git.run(
<String>['reset', commit, '--hard'],
'reset to the release commit',
workingDirectory: checkoutDirectory.path,
);
}
/// Tag [commit] and push the tag to the remote.
void tag(String commit, String tagName, String remote) {
git.run(
<String>['tag', tagName, commit],
'tag the commit with the version label',
workingDirectory: checkoutDirectory.path,
);
git.run(
<String>['push', remote, tagName],
'publish the tag to the repo',
workingDirectory: checkoutDirectory.path,
);
}
/// Push [commit] to the release channel [branch].
void updateChannel(
String commit,
String remote,
String branch, {
bool force = false,
}) {
git.run(
<String>[
'push',
if (force) '--force',
remote,
'$commit:$branch',
],
'update the release branch with the commit',
workingDirectory: checkoutDirectory.path,
);
}
Version flutterVersion() {
// Build tool
processManager.runSync(<String>[
fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'),
'help',
]);
// Check version
final io.ProcessResult result = processManager.runSync(<String>[
fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'),
'--version',
'--machine',
]);
final Map<String, dynamic> versionJson = jsonDecode(
globals.stdoutToString(result.stdout),
) as Map<String, dynamic>;
return Version.fromString(versionJson['frameworkVersion'] as String);
}
/// Create an empty commit and return the revision.
@visibleForTesting
String authorEmptyCommit([String message = 'An empty commit']) {
git.run(
<String>[
'-c',
'user.name=Conductor',
'-c',
'user.email=conductor@flutter.dev',
'commit',
'--allow-empty',
'-m',
'\'$message\'',
],
'create an empty commit',
workingDirectory: checkoutDirectory.path,
);
return reverseParse('HEAD');
}
/// Create a new clone of the current repository.
///
/// The returned repository will inherit all properties from this one, except
/// for the upstream, which will be the path to this repository on disk.
///
/// This method is for testing purposes.
@visibleForTesting
Repository cloneRepository(String cloneName) {
assert(localUpstream);
cloneName ??= 'clone-of-$name';
return Repository(
fileSystem: fileSystem,
name: cloneName,
parentDirectory: parentDirectory,
platform: platform,
processManager: processManager,
stdio: stdio,
upstream: 'file://${checkoutDirectory.path}/',
useExistingCheckout: useExistingCheckout,
);
}
}
/// An enum of all the repositories that the Conductor supports.
enum RepositoryType {
framework,
engine,
}
class Checkouts {
Checkouts({
@required Platform platform,
@required this.fileSystem,
@required this.processManager,
Directory parentDirectory,
String directoryName = 'checkouts',
}) {
if (parentDirectory != null) {
directory = parentDirectory.childDirectory(directoryName);
} else {
String filePath;
// If a test
if (platform.script.scheme == 'data') {
final RegExp pattern = RegExp(
r'(file:\/\/[^"]*[/\\]dev\/tools[/\\][^"]+\.dart)',
multiLine: true,
);
final Match match =
pattern.firstMatch(Uri.decodeFull(platform.script.path));
if (match == null) {
throw Exception(
'Cannot determine path of script!\n${platform.script.path}',
);
}
filePath = Uri.parse(match.group(1)).path.replaceAll(r'%20', ' ');
} else {
filePath = platform.script.toFilePath();
}
final String checkoutsDirname = fileSystem.path.normalize(
fileSystem.path.join(
fileSystem.path.dirname(filePath),
'..',
'checkouts',
),
);
directory = fileSystem.directory(checkoutsDirname);
}
if (!directory.existsSync()) {
directory.createSync(recursive: true);
}
}
Directory directory;
final FileSystem fileSystem;
final ProcessManager processManager;
Repository addRepo({
@required RepositoryType repoType,
@required Stdio stdio,
@required Platform platform,
FileSystem fileSystem,
String upstream,
String name,
bool localUpstream = false,
bool useExistingCheckout = false,
}) {
switch (repoType) {
case RepositoryType.framework:
name ??= 'framework';
upstream ??= 'https://github.com/flutter/flutter.git';
break;
case RepositoryType.engine:
name ??= 'engine';
upstream ??= 'https://github.com/flutter/engine.git';
break;
}
return Repository(
name: name,
upstream: upstream,
stdio: stdio,
platform: platform,
fileSystem: fileSystem,
parentDirectory: directory,
processManager: processManager,
localUpstream: localUpstream,
useExistingCheckout: useExistingCheckout,
);
}
}