Skip to content

Commit e38be67

Browse files
Update upgrade to rebase and stash local changes. (flutter#29192)
1 parent fc9f7de commit e38be67

File tree

2 files changed

+282
-19
lines changed

2 files changed

+282
-19
lines changed

packages/flutter_tools/lib/src/commands/upgrade.dart

Lines changed: 143 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ import '../version.dart';
1818
import 'channel.dart';
1919

2020
class UpgradeCommand extends FlutterCommand {
21+
UpgradeCommand() {
22+
argParser.addFlag(
23+
'force',
24+
abbr: 'f',
25+
help: 'force upgrade the flutter branch, potentially discarding local changes.',
26+
negatable: false,
27+
);
28+
}
29+
2130
@override
2231
final String name = 'upgrade';
2332

@@ -29,64 +38,182 @@ class UpgradeCommand extends FlutterCommand {
2938

3039
@override
3140
Future<FlutterCommandResult> runCommand() async {
41+
final UpgradeCommandRunner upgradeCommandRunner = UpgradeCommandRunner();
42+
await upgradeCommandRunner.runCommand(argResults['force'], GitTagVersion.determine(), FlutterVersion.instance);
43+
return null;
44+
}
45+
}
46+
47+
48+
@visibleForTesting
49+
class UpgradeCommandRunner {
50+
Future<FlutterCommandResult> runCommand(bool force, GitTagVersion gitTagVersion, FlutterVersion flutterVersion) async {
51+
await verifyUpstreamConfigured();
52+
if (!force && gitTagVersion == const GitTagVersion.unknown()) {
53+
// If the commit is a recognized branch and not master,
54+
// explain that we are avoiding potential damage.
55+
if (flutterVersion.channel != 'master' && FlutterVersion.officialChannels.contains(flutterVersion.channel)) {
56+
throwToolExit(
57+
'Unknown flutter tag. Abandoning upgrade to avoid destroying local '
58+
'changes. It is recommended to use git directly if not working off of '
59+
'an official channel.'
60+
);
61+
// Otherwise explain that local changes can be lost.
62+
} else {
63+
throwToolExit(
64+
'Unknown flutter tag. Abandoning upgrade to avoid destroying local '
65+
'changes. If it is okay to remove local changes, then re-run this '
66+
'command with --force.'
67+
);
68+
}
69+
}
70+
final String stashName = await maybeStash(gitTagVersion);
71+
await upgradeChannel(flutterVersion);
72+
await attemptRebase();
73+
await precacheArtifacts();
74+
await updatePackages(flutterVersion);
75+
await runDoctor();
76+
await applyStash(stashName);
77+
return null;
78+
}
79+
80+
/// Check if there is an upstream repository configured.
81+
///
82+
/// Exits tool if there is no upstream.
83+
Future<void> verifyUpstreamConfigured() async {
3284
try {
3385
await runCheckedAsync(<String>[
3486
'git', 'rev-parse', '@{u}',
3587
], workingDirectory: Cache.flutterRoot);
3688
} catch (e) {
37-
throwToolExit('Unable to upgrade Flutter: no upstream repository configured.');
89+
throwToolExit(
90+
'Unable to upgrade Flutter: no upstream repository configured. '
91+
'Run \'git remote add upstream '
92+
'https://github.com/flutter/flutter\' in ${Cache.flutterRoot}',
93+
);
3894
}
95+
}
3996

40-
final FlutterVersion flutterVersion = FlutterVersion.instance;
97+
/// Attempt to stash any local changes.
98+
///
99+
/// Returns the stash name if any changes were stashed. Exits tool if
100+
/// `git stash` returns a non-zero exit code.
101+
Future<String> maybeStash(GitTagVersion gitTagVersion) async {
102+
final String stashName = 'flutter-upgrade-from-v${gitTagVersion.x}.${gitTagVersion.y}.${gitTagVersion.z}';
103+
try {
104+
final RunResult runResult = await runCheckedAsync(<String>[
105+
'git', 'stash', 'push', '-m', stashName
106+
]);
107+
// output message will contain stash name if any changes were stashed..
108+
if (runResult.stdout.contains(stashName)) {
109+
return stashName;
110+
}
111+
} catch (e) {
112+
throwToolExit('Failed to stash local changes: $e');
113+
}
114+
return null;
115+
}
41116

117+
/// Attempts to upgrade the channel.
118+
///
119+
/// If the user is on a deprecated channel, attempts to migrate them off of
120+
/// it.
121+
Future<void> upgradeChannel(FlutterVersion flutterVersion) async {
42122
printStatus('Upgrading Flutter from ${Cache.flutterRoot}...');
43-
44123
await ChannelCommand.upgradeChannel();
124+
}
45125

46-
int code = await runCommandAndStreamOutput(
47-
<String>['git', 'pull', '--ff-only'],
126+
/// Attempts to rebase the upstream onto the local branch.
127+
///
128+
/// If there haven't been any hot fixes or local changes, this is equivalent
129+
/// to a fast-forward.
130+
Future<void> attemptRebase() async {
131+
final int code = await runCommandAndStreamOutput(
132+
<String>['git', 'pull', '--rebase'],
48133
workingDirectory: Cache.flutterRoot,
49134
mapFunction: (String line) => matchesGitLine(line) ? null : line,
50135
);
51-
52-
if (code != 0)
136+
if (code != 0) {
137+
printError('git rebase failed');
138+
final int undoCode = await runCommandAndStreamOutput(
139+
<String>['git', 'rebase', '--abort'],
140+
workingDirectory: Cache.flutterRoot,
141+
mapFunction: (String line) => matchesGitLine(line) ? null : line,
142+
);
143+
if (undoCode != 0) {
144+
printError(
145+
'Failed to apply rebase: The flutter installation at'
146+
' ${Cache.flutterRoot} may be corrupted. A reinstallation of Flutter '
147+
'is recommended'
148+
);
149+
}
53150
throwToolExit(null, exitCode: code);
151+
}
152+
}
54153

55-
// Check for and download any engine and pkg/ updates.
56-
// We run the 'flutter' shell script re-entrantly here
57-
// so that it will download the updated Dart and so forth
58-
// if necessary.
154+
/// Update the engine repository and precache all artifacts.
155+
///
156+
/// Check for and download any engine and pkg/ updates. We run the 'flutter'
157+
/// shell script re-entrantly here so that it will download the updated
158+
/// Dart and so forth if necessary.
159+
Future<void> precacheArtifacts() async {
59160
printStatus('');
60161
printStatus('Upgrading engine...');
61-
code = await runCommandAndStreamOutput(
162+
final int code = await runCommandAndStreamOutput(
62163
<String>[
63164
fs.path.join('bin', 'flutter'), '--no-color', '--no-version-check', 'precache',
64165
],
65166
workingDirectory: Cache.flutterRoot,
66167
allowReentrantFlutter: true,
67168
);
169+
if (code != 0) {
170+
throwToolExit(null, exitCode: code);
171+
}
172+
}
68173

174+
/// Update the user's packages.
175+
Future<void> updatePackages(FlutterVersion flutterVersion) async {
69176
printStatus('');
70177
printStatus(flutterVersion.toString());
71-
72178
final String projectRoot = findProjectRoot();
73179
if (projectRoot != null) {
74180
printStatus('');
75181
await pubGet(context: PubContext.pubUpgrade, directory: projectRoot, upgrade: true, checkLastModified: false);
76182
}
183+
}
77184

78-
// Run a doctor check in case system requirements have changed.
185+
/// Run flutter doctor in case requirements have changed.
186+
Future<void> runDoctor() async {
79187
printStatus('');
80188
printStatus('Running flutter doctor...');
81-
code = await runCommandAndStreamOutput(
189+
await runCommandAndStreamOutput(
82190
<String>[
83191
fs.path.join('bin', 'flutter'), '--no-version-check', 'doctor',
84192
],
85193
workingDirectory: Cache.flutterRoot,
86194
allowReentrantFlutter: true,
87195
);
196+
}
88197

89-
return null;
198+
/// Pop stash changes if [stashName] is non-null and contained in stash.
199+
Future<void> applyStash(String stashName) async {
200+
if (stashName == null) {
201+
return;
202+
}
203+
try {
204+
final RunResult result = await runCheckedAsync(<String>[
205+
'git', 'stash', 'list'
206+
]);
207+
if (!result.stdout.contains(stashName)) {
208+
// print the same warning as if this threw.
209+
throw Exception();
210+
}
211+
await runCheckedAsync(<String>[
212+
'git', 'stash', 'pop',
213+
]);
214+
} catch (e) {
215+
printError('Failed to re-apply local changes. State may have been lost.');
216+
}
90217
}
91218

92219
// dev/benchmarks/complex_layout/lib/main.dart | 24 +-
@@ -97,7 +224,6 @@ class UpgradeCommand extends FlutterCommand {
97224
// create mode 100644 examples/flutter_gallery/lib/gallery/demo.dart
98225
static final RegExp _gitChangedRegex = RegExp(r' (rename|delete mode|create mode) .+');
99226

100-
@visibleForTesting
101227
static bool matchesGitLine(String line) {
102228
return _gitDiffRegex.hasMatch(line)
103229
|| _gitChangedRegex.hasMatch(line)

packages/flutter_tools/test/commands/upgrade_test.dart

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,109 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter_tools/src/base/common.dart';
56
import 'package:flutter_tools/src/base/file_system.dart';
7+
import 'package:flutter_tools/src/base/io.dart';
68
import 'package:flutter_tools/src/base/os.dart';
79
import 'package:flutter_tools/src/cache.dart';
810
import 'package:flutter_tools/src/commands/upgrade.dart';
11+
import 'package:flutter_tools/src/runner/flutter_command.dart';
12+
import 'package:flutter_tools/src/version.dart';
13+
import 'package:mockito/mockito.dart';
14+
import 'package:process/process.dart';
915

1016
import '../src/common.dart';
1117
import '../src/context.dart';
1218

1319
void main() {
14-
group('upgrade', () {
20+
group('UpgradeCommandRunner', () {
21+
FakeUpgradeCommandRunner fakeCommandRunner;
22+
UpgradeCommandRunner realCommandRunner;
23+
MockProcessManager processManager;
24+
final MockFlutterVersion flutterVersion = MockFlutterVersion();
25+
const GitTagVersion gitTagVersion = GitTagVersion(1, 2, 3, 4, 5, 'asd');
26+
when(flutterVersion.channel).thenReturn('dev');
27+
28+
setUp(() {
29+
fakeCommandRunner = FakeUpgradeCommandRunner();
30+
realCommandRunner = UpgradeCommandRunner();
31+
processManager = MockProcessManager();
32+
});
33+
34+
test('throws on unknown tag, official branch, noforce', () async {
35+
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
36+
false,
37+
const GitTagVersion.unknown(),
38+
flutterVersion,
39+
);
40+
expect(result, throwsA(isInstanceOf<ToolExit>()));
41+
});
42+
43+
test('does not throw on unknown tag, official branch, force', () async {
44+
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
45+
true,
46+
const GitTagVersion.unknown(),
47+
flutterVersion,
48+
);
49+
expect(await result, null);
50+
});
51+
52+
test('Doesn\'t throw on known tag, dev branch, no force', () async {
53+
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
54+
false,
55+
gitTagVersion,
56+
flutterVersion,
57+
);
58+
expect(await result, null);
59+
});
60+
61+
test('Only pops stash if it was pushed', () async {
62+
fakeCommandRunner.stashName = 'test';
63+
final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
64+
false,
65+
gitTagVersion,
66+
flutterVersion,
67+
);
68+
expect(await result, null);
69+
expect(fakeCommandRunner.appliedStashName, 'test');
70+
});
71+
72+
testUsingContext('verifyUpstreamConfigured', () async {
73+
when(processManager.run(
74+
<String>['git', 'rev-parse', '@{u}'],
75+
environment:anyNamed('environment'),
76+
workingDirectory: anyNamed('workingDirectory'))
77+
).thenAnswer((Invocation invocation) async {
78+
return FakeProcessResult()
79+
..exitCode = 0;
80+
});
81+
await realCommandRunner.verifyUpstreamConfigured();
82+
}, overrides: <Type, Generator>{
83+
ProcessManager: () => processManager,
84+
});
85+
86+
testUsingContext('maybeStash', () async {
87+
final String stashName = 'flutter-upgrade-from-v${gitTagVersion.x}.${gitTagVersion.y}.${gitTagVersion.z}';
88+
when(processManager.run(
89+
<String>['git', 'stash', 'push', '-m', stashName],
90+
environment:anyNamed('environment'),
91+
workingDirectory: anyNamed('workingDirectory'))
92+
).thenAnswer((Invocation invocation) async {
93+
return FakeProcessResult()
94+
..exitCode = 0;
95+
});
96+
await realCommandRunner.maybeStash(gitTagVersion);
97+
}, overrides: <Type, Generator>{
98+
ProcessManager: () => processManager,
99+
});
100+
});
101+
102+
group('matchesGitLine', () {
15103
setUpAll(() {
16104
Cache.disableLocking();
17105
});
18106

19-
bool _match(String line) => UpgradeCommand.matchesGitLine(line);
107+
bool _match(String line) => UpgradeCommandRunner.matchesGitLine(line);
20108

21109
test('regex match', () {
22110
expect(_match(' .../flutter_gallery/lib/demo/buttons_demo.dart | 10 +--'), true);
@@ -63,3 +151,52 @@ void main() {
63151
});
64152
});
65153
}
154+
155+
class FakeUpgradeCommandRunner extends UpgradeCommandRunner {
156+
String stashName;
157+
String appliedStashName;
158+
159+
@override
160+
Future<void> verifyUpstreamConfigured() async {}
161+
162+
@override
163+
Future<String> maybeStash(GitTagVersion gitTagVersion) async {
164+
return stashName;
165+
}
166+
167+
@override
168+
Future<void> upgradeChannel(FlutterVersion flutterVersion) async {}
169+
170+
@override
171+
Future<void> attemptRebase() async {}
172+
173+
@override
174+
Future<void> precacheArtifacts() async {}
175+
176+
@override
177+
Future<void> updatePackages(FlutterVersion flutterVersion) async {}
178+
179+
@override
180+
Future<void> runDoctor() async {}
181+
182+
@override
183+
Future<void> applyStash(String stashName) async {
184+
appliedStashName = stashName;
185+
}
186+
}
187+
188+
class MockFlutterVersion extends Mock implements FlutterVersion {}
189+
class MockProcessManager extends Mock implements ProcessManager {}
190+
class FakeProcessResult implements ProcessResult {
191+
@override
192+
int exitCode;
193+
194+
@override
195+
int pid = 0;
196+
197+
@override
198+
String stderr = '';
199+
200+
@override
201+
String stdout = '';
202+
}

0 commit comments

Comments
 (0)