repository.dart 11.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 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.
19
abstract class Repository {
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
  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;

49
  /// Directory for the repository checkout.
50
  ///
51 52
  /// Since cloning a repository takes a long time, we do not ensure it is
  /// cloned on the filesystem until this getter is accessed.
53 54 55 56 57
  Directory get checkoutDirectory {
    if (_checkoutDirectory != null) {
      return _checkoutDirectory;
    }
    _checkoutDirectory = parentDirectory.childDirectory(name);
58 59 60 61 62 63 64 65 66 67 68 69 70 71
    if (!useExistingCheckout && _checkoutDirectory.existsSync()) {
      stdio.printTrace('Deleting $name from ${_checkoutDirectory.path}...');
      _checkoutDirectory.deleteSync(recursive: true);
    } else if (useExistingCheckout && _checkoutDirectory.existsSync()) {
      git.run(
        <String>['checkout', 'master'],
        'Checkout to master branch',
        workingDirectory: _checkoutDirectory.path,
      );
      git.run(
        <String>['pull', '--ff-only'],
        'Updating $name repo',
        workingDirectory: _checkoutDirectory.path,
      );
72
    }
73 74 75
    if (!_checkoutDirectory.existsSync()) {
      stdio.printTrace(
          'Cloning $name from $upstream to ${_checkoutDirectory.path}...');
76
      git.run(
77
        <String>['clone', '--', upstream, _checkoutDirectory.path],
78 79 80 81 82 83 84 85 86 87
        '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',
88
            workingDirectory: _checkoutDirectory.path,
89 90 91 92 93
          );
        }
      }
    }

94 95 96 97
    final String revision = reverseParse('HEAD');
    stdio
        .printTrace('Repository $name is checked out at revision "$revision".');
    return _checkoutDirectory;
98 99
  }

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  /// 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,
    );
  }

129 130 131 132 133 134 135 136
  void checkout(String revision) {
    git.run(
      <String>['checkout', revision],
      'checkout $revision',
      workingDirectory: checkoutDirectory.path,
    );
  }

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
  /// 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,
155
    ).trim();
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    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,
    );
  }

  /// 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
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
  Repository cloneRepository(String cloneName);
}

class FrameworkRepository extends Repository {
  FrameworkRepository(
    this.checkouts, {
    String name = 'framework',
    String upstream = FrameworkRepository.defaultUpstream,
    bool localUpstream = false,
    bool useExistingCheckout = false,
  }) : super(
          name: name,
          upstream: upstream,
          fileSystem: checkouts.fileSystem,
          localUpstream: localUpstream,
          parentDirectory: checkouts.directory,
          platform: checkouts.platform,
          processManager: checkouts.processManager,
          stdio: checkouts.stdio,
          useExistingCheckout: useExistingCheckout,
        );

  /// A [FrameworkRepository] with the host conductor's repo set as upstream.
  ///
  /// This is useful when testing a commit that has not been merged upstream
  /// yet.
  factory FrameworkRepository.localRepoAsUpstream(
    Checkouts checkouts, {
    String name = 'framework',
    bool useExistingCheckout = false,
    @required String upstreamPath,
  }) {
    return FrameworkRepository(
      checkouts,
      name: name,
      upstream: 'file://$upstreamPath/',
      localUpstream: false,
      useExistingCheckout: useExistingCheckout,
    );
  }

  final Checkouts checkouts;
  static const String defaultUpstream =
      'https://github.com/flutter/flutter.git';

  String get cacheDirectory => fileSystem.path.join(
        checkoutDirectory.path,
        'bin',
        'cache',
      );

  @override
308 309 310
  Repository cloneRepository(String cloneName) {
    assert(localUpstream);
    cloneName ??= 'clone-of-$name';
311 312
    return FrameworkRepository(
      checkouts,
313 314 315 316 317
      name: cloneName,
      upstream: 'file://${checkoutDirectory.path}/',
      useExistingCheckout: useExistingCheckout,
    );
  }
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369

  void _ensureToolReady() {
    final File toolsStamp =
        fileSystem.directory(cacheDirectory).childFile('flutter_tools.stamp');
    if (toolsStamp.existsSync()) {
      final String toolsStampHash = toolsStamp.readAsStringSync().trim();
      final String repoHeadHash = reverseParse('HEAD');
      if (toolsStampHash == repoHeadHash) {
        return;
      }
    }

    stdio.printTrace('Building tool...');
    // Build tool
    processManager.runSync(<String>[
      fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'),
      'help',
    ]);
  }

  io.ProcessResult runFlutter(List<String> args) {
    _ensureToolReady();

    return processManager.runSync(<String>[
      fileSystem.path.join(checkoutDirectory.path, 'bin', 'flutter'),
      ...args,
    ]);
  }

  @override
  void checkout(String revision) {
    super.checkout(revision);
    // The tool will overwrite old cached artifacts, but not delete unused
    // artifacts from a previous version. Thus, delete the entire cache and
    // re-populate.
    final Directory cache = fileSystem.directory(cacheDirectory);
    if (cache.existsSync()) {
      stdio.printTrace('Deleting cache...');
      cache.deleteSync(recursive: true);
    }
    _ensureToolReady();
  }

  Version flutterVersion() {
    // Check version
    final io.ProcessResult result =
        runFlutter(<String>['--version', '--machine']);
    final Map<String, dynamic> versionJson = jsonDecode(
      globals.stdoutToString(result.stdout),
    ) as Map<String, dynamic>;
    return Version.fromString(versionJson['frameworkVersion'] as String);
  }
370 371 372 373 374 375 376 377 378 379 380
}

/// An enum of all the repositories that the Conductor supports.
enum RepositoryType {
  framework,
  engine,
}

class Checkouts {
  Checkouts({
    @required this.fileSystem,
381
    @required this.platform,
382
    @required this.processManager,
383 384 385 386 387
    @required this.stdio,
    @required Directory parentDirectory,
    String directoryName = 'flutter_conductor_checkouts',
  })  : assert(parentDirectory != null),
        directory = parentDirectory.childDirectory(directoryName) {
388 389 390 391 392
    if (!directory.existsSync()) {
      directory.createSync(recursive: true);
    }
  }

393
  final Directory directory;
394
  final FileSystem fileSystem;
395
  final Platform platform;
396
  final ProcessManager processManager;
397
  final Stdio stdio;
398
}