roll_dev.dart 5.95 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:args/args.dart';
6 7
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
8
import 'package:meta/meta.dart';
9 10 11 12 13 14
import 'package:platform/platform.dart';

import './repository.dart';
import './stdio.dart';
import './version.dart';

15
const String kIncrement = 'increment';
16
const String kCandidateBranch = 'candidate-branch';
17 18 19 20 21 22
const String kRemoteName = 'remote';
const String kJustPrint = 'just-print';
const String kYes = 'yes';
const String kForce = 'force';
const String kSkipTagging = 'skip-tagging';

23
/// Create a new dev release without cherry picks.
24 25
class RollDevCommand extends Command<void> {
  RollDevCommand({
26 27 28 29
    required this.checkouts,
    required this.fileSystem,
    required this.platform,
    required this.stdio,
30 31 32 33 34 35 36 37 38 39 40 41 42
  }) {
    argParser.addOption(
      kIncrement,
      help: 'Specifies which part of the x.y.z version number to increment. Required.',
      valueHelp: 'level',
      allowed: <String>['y', 'z', 'm'],
      allowedHelp: <String, String>{
        'y': 'Indicates the first dev release after a beta release.',
        'z': 'Indicates a hotfix to a stable release.',
        'm': 'Indicates a standard dev release.',
      },
    );
    argParser.addOption(
43 44 45
      kCandidateBranch,
      help: 'Specifies which git branch to roll to the dev branch. Required.',
      valueHelp: 'branch',
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
      defaultsTo: null, // This option is required
    );
    argParser.addFlag(
      kForce,
      abbr: 'f',
      help: 'Force push. Necessary when the previous release had cherry-picks.',
      negatable: false,
    );
    argParser.addFlag(
      kJustPrint,
      negatable: false,
      help:
          "Don't actually roll the dev channel; "
          'just print the would-be version and quit.',
    );
    argParser.addFlag(
      kSkipTagging,
      negatable: false,
      help: 'Do not create tag and push to remote, only update release branch. '
      'For recovering when the script fails trying to git push to the release branch.'
    );
67 68 69 70 71 72 73 74 75 76 77
    argParser.addFlag(
      kYes,
      negatable: false,
      abbr: 'y',
      help: 'Skip the confirmation prompt.',
    );
    argParser.addOption(
      kRemoteName,
      help: 'Specifies which git remote to fetch from.',
      defaultsTo: 'upstream',
    );
78
  }
79

80
  final Checkouts checkouts;
81 82 83
  final FileSystem fileSystem;
  final Platform platform;
  final Stdio stdio;
84

85 86
  @override
  String get name => 'roll-dev';
87

88 89 90
  @override
  String get description =>
      'For publishing a dev release without cherry picks.';
91

92 93 94
  @override
  void run() {
    rollDev(
95
      argResults: argResults!,
96
      repository: FrameworkRepository(checkouts),
97 98
      stdio: stdio,
      usage: argParser.usage,
99 100 101 102 103 104 105
    );
  }
}

/// Main script execution.
///
/// Returns true if publishing was successful, else false.
106 107
@visibleForTesting
bool rollDev({
108 109 110 111
  required String usage,
  required ArgResults argResults,
  required Stdio stdio,
  required FrameworkRepository repository,
112
}) {
113
  final String remoteName = argResults[kRemoteName] as String;
114
  final String? level = argResults[kIncrement] as String?;
115
  final String candidateBranch = argResults[kCandidateBranch] as String;
116 117
  final bool justPrint = argResults[kJustPrint] as bool;
  final bool autoApprove = argResults[kYes] as bool;
118
  final bool force = argResults[kForce] as bool;
119
  final bool skipTagging = argResults[kSkipTagging] as bool;
120

121 122 123
  if (level == null || candidateBranch == null) {
    throw Exception(
        'roll_dev.dart --$kIncrement=level --$kCandidateBranch=branch • update the version tags '
124
        'and roll a new dev build.\n$usage');
125 126
  }

127
  final String remoteUrl = repository.remoteUrl(remoteName);
128

129
  if (!repository.gitCheckoutClean()) {
130
    throw Exception(
131 132
        'Your git repository is not clean. Try running "git clean -fd". Warning, '
        'this will delete files! Run with -n to find out which ones.');
133 134
  }

135 136 137
  repository.fetch(remoteName);

  // Verify [commit] is valid
138
  final String commit = repository.reverseParse(candidateBranch);
139

140
  stdio.printStatus('remoteName is $remoteName');
141 142 143 144
  // Get the name of the last dev release
  final Version lastVersion = Version.fromString(
    repository.getFullTag(remoteName, 'dev'),
  );
145

146
  final Version version =
147
      skipTagging ? lastVersion : Version.fromCandidateBranch(candidateBranch);
148
  final String tagName = version.toString();
149

150 151 152
  if (repository.reverseParse(lastVersion.toString()).contains(commit.trim())) {
    throw Exception(
        'Commit $commit is already on the dev branch as $lastVersion.');
153
  }
154

155
  if (justPrint) {
156
    stdio.printStatus(tagName);
157
    return false;
158 159
  }

160 161 162
  if (skipTagging && !repository.isCommitTagged(commit)) {
    throw Exception(
        'The $kSkipTagging flag is only supported for tagged commits.');
163
  }
164

165 166 167 168
  if (!force && !repository.isAncestor(commit, lastVersion.toString())) {
    throw Exception(
        'The previous dev tag $lastVersion is not a direct ancestor of $commit.\n'
        'The flag "$kForce" is required to force push a new release past a cherry-pick.');
169 170
  }

171
  final String hash = repository.reverseParse(commit);
172

173 174
  // [commit] can be a prefix for [hash].
  assert(hash.startsWith(commit));
175 176

  // PROMPT
177
  if (autoApprove) {
178 179
    stdio.printStatus(
        'Publishing Flutter $version ($hash) to the "dev" channel.');
180
  } else {
181 182 183 184 185
    stdio.printStatus('Your tree is ready to publish Flutter $version '
        '($hash) to the "dev" channel.');
    stdio.write('Are you? [yes/no] ');
    if (stdio.readLineSync() != 'yes') {
      stdio.printError('The dev roll has been aborted.');
186
      return false;
187
    }
188 189
  }

190
  if (!skipTagging) {
191
    repository.tag(commit, version.toString(), remoteName);
192
  }
193

194 195 196 197
  repository.pushRef(
    fromRef: commit,
    remote: remoteName,
    toRef: 'dev',
198
    force: force,
199 200
  );

201 202
  stdio.printStatus(
    'Flutter version $version has been rolled to the "dev" channel at $remoteUrl.',
203
  );
204
  return true;
205
}