Unverified Commit 807b6025 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_tools] Update roll_dev.dart (#59215)

parent e66a5cf5
...@@ -22,6 +22,7 @@ const String kJustPrint = 'just-print'; ...@@ -22,6 +22,7 @@ const String kJustPrint = 'just-print';
const String kYes = 'yes'; const String kYes = 'yes';
const String kHelp = 'help'; const String kHelp = 'help';
const String kForce = 'force'; const String kForce = 'force';
const String kSkipTagging = 'skip-tagging';
const String kUpstreamRemote = 'git@github.com:flutter/flutter.git'; const String kUpstreamRemote = 'git@github.com:flutter/flutter.git';
...@@ -64,6 +65,7 @@ bool run({ ...@@ -64,6 +65,7 @@ bool run({
final bool autoApprove = argResults[kYes] as bool; final bool autoApprove = argResults[kYes] as bool;
final bool help = argResults[kHelp] as bool; final bool help = argResults[kHelp] as bool;
final bool force = argResults[kForce] as bool; final bool force = argResults[kForce] as bool;
final bool skipTagging = argResults[kSkipTagging] as bool;
if (help || level == null || commit == null) { if (help || level == null || commit == null) {
print( print(
...@@ -79,8 +81,8 @@ bool run({ ...@@ -79,8 +81,8 @@ bool run({
); );
if (remote != kUpstreamRemote) { if (remote != kUpstreamRemote) {
throw Exception( throw Exception(
'The current directory is not a Flutter repository checkout with a ' 'The remote named $origin is set to $remote, when $kUpstreamRemote was '
'correctly configured upstream remote.\nFor more details see: ' 'expected.\nFor more details see: '
'https://github.com/flutter/flutter/wiki/Release-process' 'https://github.com/flutter/flutter/wiki/Release-process'
); );
} }
...@@ -92,22 +94,45 @@ bool run({ ...@@ -92,22 +94,45 @@ bool run({
); );
} }
// TODO(fujino): move this after `justPrint`
git.run('fetch $origin', 'fetch $origin'); git.run('fetch $origin', 'fetch $origin');
git.run('reset $commit --hard', 'reset to the release commit');
String version = getFullTag(git, origin); final String lastVersion = getFullTag(git, origin);
final String version = skipTagging
? lastVersion
: incrementLevel(lastVersion, level);
version = incrementLevel(version, level); if (git.getOutput(
'rev-parse $lastVersion',
'check if commit is already on dev',
).contains(commit.trim())) {
throw Exception('Commit $commit is already on the dev branch as $lastVersion.');
}
if (justPrint) { if (justPrint) {
print(version); print(version);
return false; return false;
} }
final String hash = git.getOutput('rev-parse HEAD', 'Get git hash for $commit'); if (skipTagging) {
git.run(
'describe --exact-match --tags $commit',
'verify $commit is already tagged. You can only use the flag '
'`$kSkipTagging` if the commit has already been tagged.'
);
}
git.run('tag $version', 'tag the commit with the version label'); if (!force) {
git.run(
'merge-base --is-ancestor $lastVersion $commit',
'verify $lastVersion is a direct ancestor of $commit. The flag `$kForce`'
'is required to force push a new release past a cherry-pick',
);
}
git.run('reset $commit --hard', 'reset to the release commit');
final String hash = git.getOutput('rev-parse HEAD', 'Get git hash for $commit');
// PROMPT // PROMPT
...@@ -118,13 +143,15 @@ bool run({ ...@@ -118,13 +143,15 @@ bool run({
'to the "dev" channel.'); 'to the "dev" channel.');
stdout.write('Are you? [yes/no] '); stdout.write('Are you? [yes/no] ');
if (stdin.readLineSync() != 'yes') { if (stdin.readLineSync() != 'yes') {
git.run('tag -d $version', 'remove the tag you did not want to publish');
print('The dev roll has been aborted.'); print('The dev roll has been aborted.');
return false; return false;
} }
} }
if (!skipTagging) {
git.run('tag $version', 'tag the commit with the version label');
git.run('push $origin $version', 'publish the version'); git.run('push $origin $version', 'publish the version');
}
git.run( git.run(
'push ${force ? "--force " : ""}$origin HEAD:dev', 'push ${force ? "--force " : ""}$origin HEAD:dev',
'land the new version on the "dev" branch', 'land the new version on the "dev" branch',
...@@ -170,6 +197,12 @@ ArgResults parseArguments(ArgParser argParser, List<String> args) { ...@@ -170,6 +197,12 @@ ArgResults parseArguments(ArgParser argParser, List<String> args) {
"Don't actually roll the dev channel; " "Don't actually roll the dev channel; "
'just print the would-be version and quit.', '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.'
);
argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.'); argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true); argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true);
...@@ -208,6 +241,7 @@ String getVersionFromParts(List<int> parts) { ...@@ -208,6 +241,7 @@ String getVersionFromParts(List<int> parts) {
return buf.toString(); return buf.toString();
} }
/// A wrapper around git process calls that can be mocked for unit testing.
class Git { class Git {
const Git(); const Git();
...@@ -230,16 +264,17 @@ class Git { ...@@ -230,16 +264,17 @@ class Git {
} }
void _reportFailureAndExit(ProcessResult result, String explanation) { void _reportFailureAndExit(ProcessResult result, String explanation) {
final StringBuffer message = StringBuffer();
if (result.exitCode != 0) { if (result.exitCode != 0) {
print('Failed to $explanation. Git exited with error code ${result.exitCode}.'); message.writeln('Failed to $explanation. Git exited with error code ${result.exitCode}.');
} else { } else {
print('Failed to $explanation.'); message.writeln('Failed to $explanation.');
} }
if ((result.stdout as String).isNotEmpty) if ((result.stdout as String).isNotEmpty)
print('stdout from git:\n${result.stdout}\n'); message.writeln('stdout from git:\n${result.stdout}\n');
if ((result.stderr as String).isNotEmpty) if ((result.stderr as String).isNotEmpty)
print('stderr from git:\n${result.stderr}\n'); message.writeln('stderr from git:\n${result.stderr}\n');
exit(1); throw Exception(message);
} }
} }
......
...@@ -14,6 +14,8 @@ void main() { ...@@ -14,6 +14,8 @@ void main() {
const String level = 'z'; const String level = 'z';
const String commit = 'abcde012345'; const String commit = 'abcde012345';
const String origin = 'upstream'; const String origin = 'upstream';
const String lastVersion = '1.2.0-0.0.pre';
const String nextVersion = '1.2.0-1.0.pre';
FakeArgResults fakeArgResults; FakeArgResults fakeArgResults;
MockGit mockGit; MockGit mockGit;
...@@ -26,8 +28,6 @@ void main() { ...@@ -26,8 +28,6 @@ void main() {
level: level, level: level,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: true, help: true,
); );
expect( expect(
...@@ -42,12 +42,9 @@ void main() { ...@@ -42,12 +42,9 @@ void main() {
test('returns false if level not provided', () { test('returns false if level not provided', () {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: null,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: true,
); );
expect( expect(
run( run(
...@@ -62,11 +59,8 @@ void main() { ...@@ -62,11 +59,8 @@ void main() {
test('returns false if commit not provided', () { test('returns false if commit not provided', () {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, commit: null,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: true,
); );
expect( expect(
run( run(
...@@ -79,28 +73,22 @@ void main() { ...@@ -79,28 +73,22 @@ void main() {
}); });
test('throws exception if upstream remote wrong', () { test('throws exception if upstream remote wrong', () {
when(mockGit.getOutput('remote get-url $origin', any)).thenReturn('wrong-remote'); const String remote = 'wrong-remote';
when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(remote);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: false,
); );
Exception exception; const String errorMessage = 'The remote named $origin is set to $remote, when $kUpstreamRemote was expected.';
try { expect(
run( () => run(
usage: usage, usage: usage,
argResults: fakeArgResults, argResults: fakeArgResults,
git: mockGit, git: mockGit,
),
throwsExceptionWith(errorMessage),
); );
} on Exception catch (e) {
exception = e;
}
const String pattern = r'The current directory is not a Flutter '
'repository checkout with a correctly configured upstream remote.';
expect(exception?.toString(), contains(pattern));
}); });
test('throws exception if git checkout not clean', () { test('throws exception if git checkout not clean', () {
...@@ -112,9 +100,6 @@ void main() { ...@@ -112,9 +100,6 @@ void main() {
level: level, level: level,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: false,
); );
Exception exception; Exception exception;
try { try {
...@@ -132,20 +117,22 @@ void main() { ...@@ -132,20 +117,22 @@ void main() {
expect(exception?.toString(), contains(pattern)); expect(exception?.toString(), contains(pattern));
}); });
test('does not tag if --just-print is specified', () { test('does not reset or tag if --just-print is specified', () {
when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
when(mockGit.getOutput('status --porcelain', any)).thenReturn(''); when(mockGit.getOutput('status --porcelain', any)).thenReturn('');
when(mockGit.getOutput( when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any, any,
)).thenReturn('1.2.3-0.0.pre'); )).thenReturn(lastVersion);
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn('zxy321');
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: true, justPrint: true,
autoApprove: true,
help: false,
); );
expect(run( expect(run(
usage: usage, usage: usage,
...@@ -153,25 +140,172 @@ void main() { ...@@ -153,25 +140,172 @@ void main() {
git: mockGit, git: mockGit,
), false); ), false);
verify(mockGit.run('fetch $origin', any)); verify(mockGit.run('fetch $origin', any));
verify(mockGit.run('reset $commit --hard', any)); verifyNever(mockGit.run('reset $commit --hard', any));
verifyNever(mockGit.getOutput('rev-parse HEAD', any)); verifyNever(mockGit.getOutput('rev-parse HEAD', any));
}); });
test('successfully tags and publishes release', () { test('exits with exception if --skip-tagging is provided but commit isn\'t '
'already tagged', () {
when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
when(mockGit.getOutput('status --porcelain', any)).thenReturn('');
when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any,
)).thenReturn(lastVersion);
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn('zxy321');
const String exceptionMessage = 'Failed to verify $commit is already '
'tagged. You can only use the flag `$kSkipTagging` if the commit has '
'already been tagged.';
when(mockGit.run(
'describe --exact-match --tags $commit',
any,
)).thenThrow(Exception(exceptionMessage));
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
origin: origin,
skipTagging: true,
);
expect(
() => run(
usage: usage,
argResults: fakeArgResults,
git: mockGit,
),
throwsExceptionWith(exceptionMessage),
);
verify(mockGit.run('fetch $origin', any));
verifyNever(mockGit.run('reset $commit --hard', any));
verifyNever(mockGit.getOutput('rev-parse HEAD', any));
});
test('throws exception if desired commit is already tip of dev branch', () {
when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote); when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
when(mockGit.getOutput('status --porcelain', any)).thenReturn(''); when(mockGit.getOutput('status --porcelain', any)).thenReturn('');
when(mockGit.getOutput( when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any, any,
)).thenReturn('1.2.3-0.0.pre'); )).thenReturn(lastVersion);
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn(commit);
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
origin: origin,
justPrint: true,
);
expect(
() => run(
usage: usage,
argResults: fakeArgResults,
git: mockGit,
),
throwsExceptionWith('is already on the dev branch as'),
);
verify(mockGit.run('fetch $origin', any));
verifyNever(mockGit.run('reset $commit --hard', any));
verifyNever(mockGit.getOutput('rev-parse HEAD', any));
});
test('does not tag if last release is not direct ancestor of desired '
'commit and --force not supplied', () {
when(mockGit.getOutput('remote get-url $origin', any))
.thenReturn(kUpstreamRemote);
when(mockGit.getOutput('status --porcelain', any))
.thenReturn('');
when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any,
)).thenReturn(lastVersion);
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn('zxy321');
when(mockGit.run('merge-base --is-ancestor $lastVersion $commit', any))
.thenThrow(Exception(
'Failed to verify $lastVersion is a direct ancestor of $commit. The '
'flag `--force` is required to force push a new release past a '
'cherry-pick',
));
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
origin: origin,
);
const String errorMessage = 'Failed to verify $lastVersion is a direct '
'ancestor of $commit. The flag `--force` is required to force push a '
'new release past a cherry-pick';
expect(
() => run(
argResults: fakeArgResults,
git: mockGit,
usage: usage,
),
throwsExceptionWith(errorMessage),
);
verify(mockGit.run('fetch $origin', any));
verifyNever(mockGit.run('reset $commit --hard', any));
verifyNever(mockGit.run('push $origin HEAD:dev', any));
verifyNever(mockGit.run('tag $nextVersion', any));
});
test('does not tag but updates branch if --skip-tagging provided', () {
when(mockGit.getOutput('remote get-url $origin', any))
.thenReturn(kUpstreamRemote);
when(mockGit.getOutput('status --porcelain', any))
.thenReturn('');
when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any,
)).thenReturn(lastVersion);
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn('zxy321');
when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit);
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
origin: origin,
skipTagging: true,
);
expect(run(
usage: usage,
argResults: fakeArgResults,
git: mockGit,
), true);
verify(mockGit.run('fetch $origin', any));
verify(mockGit.run('reset $commit --hard', any));
verifyNever(mockGit.run('tag $nextVersion', any));
verifyNever(mockGit.run('push $origin $nextVersion', any));
verify(mockGit.run('push $origin HEAD:dev', any));
});
test('successfully tags and publishes release', () {
when(mockGit.getOutput('remote get-url $origin', any))
.thenReturn(kUpstreamRemote);
when(mockGit.getOutput('status --porcelain', any))
.thenReturn('');
when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any,
)).thenReturn('1.2.0-0.0.pre');
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn('zxy321');
when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit); when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: false,
); );
expect(run( expect(run(
usage: usage, usage: usage,
...@@ -180,7 +314,8 @@ void main() { ...@@ -180,7 +314,8 @@ void main() {
), true); ), true);
verify(mockGit.run('fetch $origin', any)); verify(mockGit.run('fetch $origin', any));
verify(mockGit.run('reset $commit --hard', any)); verify(mockGit.run('reset $commit --hard', any));
verify(mockGit.run('tag 1.2.0-1.0.pre', any)); verify(mockGit.run('tag $nextVersion', any));
verify(mockGit.run('push $origin $nextVersion', any));
verify(mockGit.run('push $origin HEAD:dev', any)); verify(mockGit.run('push $origin HEAD:dev', any));
}); });
...@@ -190,15 +325,16 @@ void main() { ...@@ -190,15 +325,16 @@ void main() {
when(mockGit.getOutput( when(mockGit.getOutput(
'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev', 'describe --match *.*.*-*.*.pre --exact-match --tags refs/remotes/$origin/dev',
any, any,
)).thenReturn('1.2.3-0.0.pre'); )).thenReturn(lastVersion);
when(mockGit.getOutput(
'rev-parse $lastVersion',
any,
)).thenReturn('zxy321');
when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit); when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, commit: commit,
origin: origin, origin: origin,
justPrint: false,
autoApprove: true,
help: false,
force: true, force: true,
); );
expect(run( expect(run(
...@@ -208,7 +344,7 @@ void main() { ...@@ -208,7 +344,7 @@ void main() {
), true); ), true);
verify(mockGit.run('fetch $origin', any)); verify(mockGit.run('fetch $origin', any));
verify(mockGit.run('reset $commit --hard', any)); verify(mockGit.run('reset $commit --hard', any));
verify(mockGit.run('tag 1.2.0-1.0.pre', any)); verify(mockGit.run('tag $nextVersion', any));
verify(mockGit.run('push --force $origin HEAD:dev', any)); verify(mockGit.run('push --force $origin HEAD:dev', any));
}); });
}); });
...@@ -268,14 +404,14 @@ void main() { ...@@ -268,14 +404,14 @@ void main() {
String version = '1.0.0-0.0.pre-1-g$hash'; String version = '1.0.0-0.0.pre-1-g$hash';
expect( expect(
() => incrementLevel(version, level), () => incrementLevel(version, level),
throwsException, throwsExceptionWith('Git reported the latest version as "$version"'),
reason: 'should throw because $version should be an exact tag', reason: 'should throw because $version should be an exact tag',
); );
version = '1.2.3'; version = '1.2.3';
expect( expect(
() => incrementLevel(version, level), () => incrementLevel(version, level),
throwsException, throwsExceptionWith('Git reported the latest version as "$version"'),
reason: 'should throw because $version should be a dev tag, not stable.' reason: 'should throw because $version should be a dev tag, not stable.'
); );
...@@ -283,7 +419,7 @@ void main() { ...@@ -283,7 +419,7 @@ void main() {
level = 'q'; level = 'q';
expect( expect(
() => incrementLevel(version, level), () => incrementLevel(version, level),
throwsException, throwsExceptionWith('Git reported the latest version as "$version"'),
reason: 'should throw because $level is unsupported', reason: 'should throw because $level is unsupported',
); );
}); });
...@@ -329,15 +465,26 @@ void main() { ...@@ -329,15 +465,26 @@ void main() {
}); });
} }
Matcher throwsExceptionWith(String messageSubString) {
return throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'description',
contains(messageSubString),
),
);
}
class FakeArgResults implements ArgResults { class FakeArgResults implements ArgResults {
FakeArgResults({ FakeArgResults({
String level, String level,
String commit, String commit,
String origin, String origin,
bool justPrint, bool justPrint = false,
bool autoApprove, bool autoApprove = true, // so we don't have to mock stdin
bool help, bool help = false,
bool force = false, bool force = false,
bool skipTagging = false,
}) : _parsedArgs = <String, dynamic>{ }) : _parsedArgs = <String, dynamic>{
'increment': level, 'increment': level,
'commit': commit, 'commit': commit,
...@@ -346,6 +493,7 @@ class FakeArgResults implements ArgResults { ...@@ -346,6 +493,7 @@ class FakeArgResults implements ArgResults {
'yes': autoApprove, 'yes': autoApprove,
'help': help, 'help': help,
'force': force, 'force': force,
'skip-tagging': skipTagging,
}; };
@override @override
......
...@@ -247,7 +247,6 @@ void main() { ...@@ -247,7 +247,6 @@ void main() {
timeout: timeout, timeout: timeout,
timeoutRetries: 0, timeoutRetries: 0,
), throwsA(isA<ProcessException>())); ), throwsA(isA<ProcessException>()));
time.elapse(timeout); time.elapse(timeout);
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment