Skip to content

Commit d56af3c

Browse files
Add --force to roll_dev.dart (flutter#56501)
1 parent 7ba5078 commit d56af3c

File tree

2 files changed

+521
-136
lines changed

2 files changed

+521
-136
lines changed

dev/tools/lib/roll_dev.dart

+180-122
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import 'dart:io';
1111

1212
import 'package:args/args.dart';
13+
import 'package:meta/meta.dart';
1314

1415
const String kIncrement = 'increment';
1516
const String kX = 'x';
@@ -20,131 +21,93 @@ const String kOrigin = 'origin';
2021
const String kJustPrint = 'just-print';
2122
const String kYes = 'yes';
2223
const String kHelp = 'help';
24+
const String kForce = 'force';
2325

2426
const String kUpstreamRemote = 'git@github.com:flutter/flutter.git';
2527

2628
void main(List<String> args) {
2729
final ArgParser argParser = ArgParser(allowTrailingOptions: false);
2830

29-
argParser.addOption(
30-
kIncrement,
31-
help: 'Specifies which part of the x.y.z version number to increment. Required.',
32-
valueHelp: 'level',
33-
allowed: <String>[kX, kY, kZ],
34-
allowedHelp: <String, String>{
35-
kX: 'Indicates a major development, e.g. typically changed after a big press event.',
36-
kY: 'Indicates a minor development, e.g. typically changed after a beta release.',
37-
kZ: 'Indicates the least notable level of change. You normally want this.',
38-
},
39-
);
40-
argParser.addOption(
41-
kCommit,
42-
help: 'Specifies which git commit to roll to the dev branch. Required.',
43-
valueHelp: 'hash',
44-
defaultsTo: null, // This option is required
45-
);
46-
argParser.addOption(
47-
kOrigin,
48-
help: 'Specifies the name of the upstream repository',
49-
valueHelp: 'repository',
50-
defaultsTo: 'upstream',
51-
);
52-
argParser.addFlag(
53-
kJustPrint,
54-
negatable: false,
55-
help:
56-
"Don't actually roll the dev channel; "
57-
'just print the would-be version and quit.',
58-
);
59-
argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
60-
argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true);
61-
6231
ArgResults argResults;
6332
try {
64-
argResults = argParser.parse(args);
33+
argResults = parseArguments(argParser, args);
6534
} on ArgParserException catch (error) {
6635
print(error.message);
6736
print(argParser.usage);
6837
exit(1);
6938
}
7039

40+
try {
41+
run(
42+
usage: argParser.usage,
43+
argResults: argResults,
44+
git: const Git(),
45+
);
46+
} on Exception catch (e) {
47+
print(e.toString());
48+
exit(1);
49+
}
50+
}
51+
52+
/// Main script execution.
53+
///
54+
/// Returns true if publishing was successful, else false.
55+
bool run({
56+
@required String usage,
57+
@required ArgResults argResults,
58+
@required Git git,
59+
}) {
7160
final String level = argResults[kIncrement] as String;
7261
final String commit = argResults[kCommit] as String;
7362
final String origin = argResults[kOrigin] as String;
7463
final bool justPrint = argResults[kJustPrint] as bool;
7564
final bool autoApprove = argResults[kYes] as bool;
7665
final bool help = argResults[kHelp] as bool;
66+
final bool force = argResults[kForce] as bool;
7767

7868
if (help || level == null || commit == null) {
79-
print('roll_dev.dart --increment=level --commit=hash • update the version tags and roll a new dev build.\n');
80-
print(argParser.usage);
81-
exit(0);
69+
print(
70+
'roll_dev.dart --increment=level --commit=hash • update the version tags '
71+
'and roll a new dev build.\n$usage'
72+
);
73+
return false;
8274
}
8375

84-
if (getGitOutput('remote get-url $origin', 'check whether this is a flutter checkout') != kUpstreamRemote) {
85-
print('The current directory is not a Flutter repository checkout with a correctly configured upstream remote.');
86-
print('For more details see: https://github.com/flutter/flutter/wiki/Release-process');
87-
exit(1);
76+
final String remote = git.getOutput(
77+
'remote get-url $origin',
78+
'check whether this is a flutter checkout',
79+
);
80+
if (remote != kUpstreamRemote) {
81+
throw Exception(
82+
'The current directory is not a Flutter repository checkout with a '
83+
'correctly configured upstream remote.\nFor more details see: '
84+
'https://github.com/flutter/flutter/wiki/Release-process'
85+
);
8886
}
8987

90-
if (getGitOutput('status --porcelain', 'check status of your local checkout') != '') {
91-
print('Your git repository is not clean. Try running "git clean -fd". Warning, this ');
92-
print('will delete files! Run with -n to find out which ones.');
93-
exit(1);
88+
if (git.getOutput('status --porcelain', 'check status of your local checkout') != '') {
89+
throw Exception(
90+
'Your git repository is not clean. Try running "git clean -fd". Warning, '
91+
'this will delete files! Run with -n to find out which ones.'
92+
);
9493
}
9594

96-
runGit('fetch $origin', 'fetch $origin');
97-
runGit('reset $commit --hard', 'reset to the release commit');
95+
// TODO(fujino): move this after `justPrint`
96+
git.run('fetch $origin', 'fetch $origin');
97+
git.run('reset $commit --hard', 'reset to the release commit');
9898

99-
String version = getFullTag();
100-
final Match match = parseFullTag(version);
101-
if (match == null) {
102-
print('Could not determine the version for this build.');
103-
if (version.isNotEmpty)
104-
print('Git reported the latest version as "$version", which does not fit the expected pattern.');
105-
exit(1);
106-
}
99+
String version = getFullTag(git);
107100

108-
final List<int> parts = match.groups(<int>[1, 2, 3, 4, 5]).map<int>(int.parse).toList();
109-
110-
if (match.group(6) == '0') {
111-
print('This commit has already been released, as version ${getVersionFromParts(parts)}.');
112-
exit(0);
113-
}
114-
115-
switch (level) {
116-
case kX:
117-
parts[0] += 1;
118-
parts[1] = 0;
119-
parts[2] = 0;
120-
parts[3] = 0;
121-
parts[4] = 0;
122-
break;
123-
case kY:
124-
parts[1] += 1;
125-
parts[2] = 0;
126-
parts[3] = 0;
127-
parts[4] = 0;
128-
break;
129-
case kZ:
130-
parts[2] = 0;
131-
parts[3] += 1;
132-
parts[4] = 0;
133-
break;
134-
default:
135-
print('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".');
136-
exit(1);
137-
}
138-
version = getVersionFromParts(parts);
101+
version = incrementLevel(version, level);
139102

140103
if (justPrint) {
141104
print(version);
142-
exit(0);
105+
return false;
143106
}
144107

145-
final String hash = getGitOutput('rev-parse HEAD', 'Get git hash for $commit');
108+
final String hash = git.getOutput('rev-parse HEAD', 'Get git hash for $commit');
146109

147-
runGit('tag $version', 'tag the commit with the version label');
110+
git.run('tag $version', 'tag the commit with the version label');
148111

149112
// PROMPT
150113

@@ -155,29 +118,79 @@ void main(List<String> args) {
155118
'to the "dev" channel.');
156119
stdout.write('Are you? [yes/no] ');
157120
if (stdin.readLineSync() != 'yes') {
158-
runGit('tag -d $version', 'remove the tag you did not want to publish');
121+
git.run('tag -d $version', 'remove the tag you did not want to publish');
159122
print('The dev roll has been aborted.');
160-
exit(0);
123+
return false;
161124
}
162125
}
163126

164-
runGit('push $origin $version', 'publish the version');
165-
runGit('push $origin HEAD:dev', 'land the new version on the "dev" branch');
127+
git.run('push $origin $version', 'publish the version');
128+
git.run(
129+
'push ${force ? "--force " : ""}$origin HEAD:dev',
130+
'land the new version on the "dev" branch',
131+
);
166132
print('Flutter version $version has been rolled to the "dev" channel!');
133+
return true;
134+
}
135+
136+
ArgResults parseArguments(ArgParser argParser, List<String> args) {
137+
argParser.addOption(
138+
kIncrement,
139+
help: 'Specifies which part of the x.y.z version number to increment. Required.',
140+
valueHelp: 'level',
141+
allowed: <String>[kX, kY, kZ],
142+
allowedHelp: <String, String>{
143+
kX: 'Indicates a major development, e.g. typically changed after a big press event.',
144+
kY: 'Indicates a minor development, e.g. typically changed after a beta release.',
145+
kZ: 'Indicates the least notable level of change. You normally want this.',
146+
},
147+
);
148+
argParser.addOption(
149+
kCommit,
150+
help: 'Specifies which git commit to roll to the dev branch. Required.',
151+
valueHelp: 'hash',
152+
defaultsTo: null, // This option is required
153+
);
154+
argParser.addOption(
155+
kOrigin,
156+
help: 'Specifies the name of the upstream repository',
157+
valueHelp: 'repository',
158+
defaultsTo: 'upstream',
159+
);
160+
argParser.addFlag(
161+
kForce,
162+
abbr: 'f',
163+
help: 'Force push. Necessary when the previous release had cherry-picks.',
164+
negatable: false,
165+
);
166+
argParser.addFlag(
167+
kJustPrint,
168+
negatable: false,
169+
help:
170+
"Don't actually roll the dev channel; "
171+
'just print the would-be version and quit.',
172+
);
173+
argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
174+
argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true);
175+
176+
return argParser.parse(args);
167177
}
168178

169-
String getFullTag() {
179+
/// Obtain the version tag of the previous dev release.
180+
String getFullTag(Git git) {
170181
const String glob = '*.*.*-*.*.pre';
171-
return getGitOutput(
172-
'describe --match $glob --first-parent --long --tags',
182+
// describe the latest dev release
183+
const String ref = 'refs/heads/dev';
184+
return git.getOutput(
185+
'describe --match $glob --exact-match --tags $ref',
173186
'obtain last released version number',
174187
);
175188
}
176189

177190
Match parseFullTag(String version) {
178-
// of the form: x.y.z-m.n.pre-c-g<revision>
191+
// of the form: x.y.z-m.n.pre
179192
final RegExp versionPattern = RegExp(
180-
r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre-(\d+)-g([a-f0-9]+)$');
193+
r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$');
181194
return versionPattern.matchAsPrefix(version);
182195
}
183196

@@ -195,33 +208,78 @@ String getVersionFromParts(List<int> parts) {
195208
return buf.toString();
196209
}
197210

198-
String getGitOutput(String command, String explanation) {
199-
final ProcessResult result = _runGit(command);
200-
if ((result.stderr as String).isEmpty && result.exitCode == 0)
201-
return (result.stdout as String).trim();
202-
_reportGitFailureAndExit(result, explanation);
203-
return null; // for the analyzer's sake
204-
}
211+
class Git {
212+
const Git();
205213

206-
void runGit(String command, String explanation) {
207-
final ProcessResult result = _runGit(command);
208-
if (result.exitCode != 0)
209-
_reportGitFailureAndExit(result, explanation);
210-
}
214+
String getOutput(String command, String explanation) {
215+
final ProcessResult result = _run(command);
216+
if ((result.stderr as String).isEmpty && result.exitCode == 0)
217+
return (result.stdout as String).trim();
218+
_reportFailureAndExit(result, explanation);
219+
return null; // for the analyzer's sake
220+
}
221+
222+
void run(String command, String explanation) {
223+
final ProcessResult result = _run(command);
224+
if (result.exitCode != 0)
225+
_reportFailureAndExit(result, explanation);
226+
}
227+
228+
ProcessResult _run(String command) {
229+
return Process.runSync('git', command.split(' '));
230+
}
211231

212-
ProcessResult _runGit(String command) {
213-
return Process.runSync('git', command.split(' '));
232+
void _reportFailureAndExit(ProcessResult result, String explanation) {
233+
if (result.exitCode != 0) {
234+
print('Failed to $explanation. Git exited with error code ${result.exitCode}.');
235+
} else {
236+
print('Failed to $explanation.');
237+
}
238+
if ((result.stdout as String).isNotEmpty)
239+
print('stdout from git:\n${result.stdout}\n');
240+
if ((result.stderr as String).isNotEmpty)
241+
print('stderr from git:\n${result.stderr}\n');
242+
exit(1);
243+
}
214244
}
215245

216-
void _reportGitFailureAndExit(ProcessResult result, String explanation) {
217-
if (result.exitCode != 0) {
218-
print('Failed to $explanation. Git exited with error code ${result.exitCode}.');
219-
} else {
220-
print('Failed to $explanation.');
246+
/// Return a copy of the [version] with [level] incremented by one.
247+
String incrementLevel(String version, String level) {
248+
final Match match = parseFullTag(version);
249+
if (match == null) {
250+
String errorMessage;
251+
if (version.isEmpty) {
252+
errorMessage = 'Could not determine the version for this build.';
253+
} else {
254+
errorMessage = 'Git reported the latest version as "$version", which '
255+
'does not fit the expected pattern.';
256+
}
257+
throw Exception(errorMessage);
258+
}
259+
260+
final List<int> parts = match.groups(<int>[1, 2, 3, 4, 5]).map<int>(int.parse).toList();
261+
262+
switch (level) {
263+
case kX:
264+
parts[0] += 1;
265+
parts[1] = 0;
266+
parts[2] = 0;
267+
parts[3] = 0;
268+
parts[4] = 0;
269+
break;
270+
case kY:
271+
parts[1] += 1;
272+
parts[2] = 0;
273+
parts[3] = 0;
274+
parts[4] = 0;
275+
break;
276+
case kZ:
277+
parts[2] = 0;
278+
parts[3] += 1;
279+
parts[4] = 0;
280+
break;
281+
default:
282+
throw Exception('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".');
221283
}
222-
if ((result.stdout as String).isNotEmpty)
223-
print('stdout from git:\n${result.stdout}\n');
224-
if ((result.stderr as String).isNotEmpty)
225-
print('stderr from git:\n${result.stderr}\n');
226-
exit(1);
284+
return getVersionFromParts(parts);
227285
}

0 commit comments

Comments
 (0)