Skip to content

Commit 527db4c

Browse files
authored
feat: avoid autoinstalling manually uninstalled commands (#73)
1 parent 7675b26 commit 527db4c

File tree

8 files changed

+436
-28
lines changed

8 files changed

+436
-28
lines changed

lib/src/command_runner/commands/install_completion_files_command.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class InstallCompletionFilesCommand<T> extends Command<T> {
4949
FutureOr<T>? run() {
5050
final verbose = argResults!['verbose'] as bool;
5151
final level = verbose ? Level.verbose : Level.info;
52-
runner.tryInstallCompletionFiles(level);
52+
runner.tryInstallCompletionFiles(level, force: true);
5353
return null;
5454
}
5555
}

lib/src/command_runner/completion_command_runner.dart

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,18 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
6565
return _completionInstallation = completionInstallation;
6666
}
6767

68+
/// The list of commands that should not trigger the auto installation.
69+
static const _reservedCommands = {
70+
HandleCompletionRequestCommand.commandName,
71+
InstallCompletionFilesCommand.commandName,
72+
UnistallCompletionFilesCommand.commandName,
73+
};
74+
6875
@override
6976
@mustCallSuper
7077
Future<T?> runCommand(ArgResults topLevelResults) async {
71-
final reservedCommands = [
72-
HandleCompletionRequestCommand.commandName,
73-
InstallCompletionFilesCommand.commandName,
74-
];
75-
7678
if (enableAutoInstall &&
77-
!reservedCommands.contains(topLevelResults.command?.name)) {
79+
!_reservedCommands.contains(topLevelResults.command?.name)) {
7880
// When auto installing, use error level to display messages.
7981
tryInstallCompletionFiles(Level.error);
8082
}
@@ -84,10 +86,10 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
8486

8587
/// Tries to install completion files for the current shell.
8688
@internal
87-
void tryInstallCompletionFiles(Level level) {
89+
void tryInstallCompletionFiles(Level level, {bool force = false}) {
8890
try {
8991
completionInstallationLogger.level = level;
90-
completionInstallation.install(executableName);
92+
completionInstallation.install(executableName, force: force);
9193
} on CompletionInstallationException catch (e) {
9294
completionInstallationLogger.warn(e.toString());
9395
} on Exception catch (e) {

lib/src/installer/completion_configuration.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,54 @@ String _jsonEncodeUninstalls(Uninstalls uninstalls) {
137137
entry.key.toString(): entry.value.toList(),
138138
});
139139
}
140+
141+
/// Provides convinience methods for [Uninstalls].
142+
extension UninstallsExtension on Uninstalls {
143+
/// Returns a new [Uninstalls] with the given [command] added to
144+
/// [systemShell].
145+
Uninstalls include({
146+
required String command,
147+
required SystemShell systemShell,
148+
}) {
149+
final modifiable = _modifiable();
150+
151+
if (modifiable.containsKey(systemShell)) {
152+
modifiable[systemShell]!.add(command);
153+
} else {
154+
modifiable[systemShell] = {command};
155+
}
156+
157+
return UnmodifiableMapView(
158+
modifiable.map((key, value) => MapEntry(key, UnmodifiableSetView(value))),
159+
);
160+
}
161+
162+
/// Returns a new [Uninstalls] with the given [command] removed from
163+
/// [systemShell].
164+
Uninstalls exclude({
165+
required String command,
166+
required SystemShell systemShell,
167+
}) {
168+
final modifiable = _modifiable();
169+
170+
if (modifiable.containsKey(systemShell)) {
171+
modifiable[systemShell]!.remove(command);
172+
}
173+
174+
return UnmodifiableMapView(
175+
modifiable.map((key, value) => MapEntry(key, UnmodifiableSetView(value))),
176+
);
177+
}
178+
179+
/// Whether the [command] is contained in [systemShell].
180+
bool contains({required String command, required SystemShell systemShell}) {
181+
if (containsKey(systemShell)) {
182+
return this[systemShell]!.contains(command);
183+
}
184+
return false;
185+
}
186+
187+
Map<SystemShell, Set<String>> _modifiable() {
188+
return map((key, value) => MapEntry(key, value.toSet()));
189+
}
190+
}

lib/src/installer/completion_installation.dart

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ class CompletionInstallation {
9090
}
9191
}
9292

93+
/// Define the [File] in which the completion configuration is stored.
94+
@visibleForTesting
95+
File get completionConfigurationFile {
96+
return File(path.join(completionConfigDir.path, 'config.json'));
97+
}
98+
9399
/// Install completion configuration files for a [rootCommand] in the
94100
/// current shell.
95101
///
@@ -101,7 +107,11 @@ class CompletionInstallation {
101107
/// completion script file.
102108
/// - A line in the shell config file (e.g. `.bash_profile`) that sources
103109
/// the aforementioned config file.
104-
void install(String rootCommand) {
110+
///
111+
/// If [force] is true, it will overwrite the command's completion files even
112+
/// if they already exist. If false, it will check if it has been explicitly
113+
/// uninstalled before installing it.
114+
void install(String rootCommand, {bool force = false}) {
105115
final configuration = this.configuration;
106116

107117
if (configuration == null) {
@@ -111,6 +121,10 @@ class CompletionInstallation {
111121
);
112122
}
113123

124+
if (!force && !_shouldInstall(rootCommand)) {
125+
return;
126+
}
127+
114128
logger.detail(
115129
'Installing completion for the command $rootCommand '
116130
'on ${configuration.shell.name}',
@@ -124,6 +138,33 @@ class CompletionInstallation {
124138
if (completionFileCreated) {
125139
_logSourceInstructions(rootCommand);
126140
}
141+
142+
final completionConfiguration =
143+
CompletionConfiguration.fromFile(completionConfigurationFile);
144+
completionConfiguration
145+
.copyWith(
146+
uninstalls: completionConfiguration.uninstalls.exclude(
147+
command: rootCommand,
148+
systemShell: configuration.shell,
149+
),
150+
)
151+
.writeTo(completionConfigurationFile);
152+
}
153+
154+
/// Wether the completion configuration files for a [rootCommand] should be
155+
/// installed or not.
156+
///
157+
/// It will return false if the root command has been explicitly uninstalled.
158+
bool _shouldInstall(String rootCommand) {
159+
final completionConfiguration = CompletionConfiguration.fromFile(
160+
completionConfigurationFile,
161+
);
162+
final systemShell = configuration!.shell;
163+
final isUninstalled = completionConfiguration.uninstalls.contains(
164+
command: rootCommand,
165+
systemShell: systemShell,
166+
);
167+
return !isUninstalled;
127168
}
128169

129170
/// Create a directory in which the completion config files shall be saved.
@@ -378,9 +419,23 @@ ${configuration!.sourceLineTemplate(scriptPath)}''';
378419
if (!shellCompletionConfigurationFile.existsSync()) {
379420
completionEntry.removeFrom(shellRCFile);
380421
}
381-
382-
if (completionConfigDir.listSync().isEmpty) {
383-
completionConfigDir.deleteSync();
422+
final completionConfigDirContent = completionConfigDir.listSync();
423+
final onlyHasConfigurationFile = completionConfigDirContent.length == 1 &&
424+
path.absolute(completionConfigDirContent.first.path) ==
425+
path.absolute(completionConfigurationFile.path);
426+
if (completionConfigDirContent.isEmpty || onlyHasConfigurationFile) {
427+
completionConfigDir.deleteSync(recursive: true);
428+
} else {
429+
final completionConfiguration =
430+
CompletionConfiguration.fromFile(completionConfigurationFile);
431+
completionConfiguration
432+
.copyWith(
433+
uninstalls: completionConfiguration.uninstalls.include(
434+
command: rootCommand,
435+
systemShell: configuration.shell,
436+
),
437+
)
438+
.writeTo(completionConfigurationFile);
384439
}
385440
}
386441
}

test/src/command_runner/commands/install_completion_files_command_test.dart

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ import 'package:mason_logger/mason_logger.dart';
44
import 'package:mocktail/mocktail.dart';
55
import 'package:test/test.dart';
66

7-
class MockLogger extends Mock implements Logger {}
7+
class _MockLogger extends Mock implements Logger {}
88

9-
class MockCompletionInstallation extends Mock
9+
class _MockCompletionInstallation extends Mock
1010
implements CompletionInstallation {}
1111

1212
class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
1313
_TestCompletionCommandRunner() : super('test', 'Test command runner');
1414

1515
@override
1616
// ignore: overridden_fields
17-
final Logger completionInstallationLogger = MockLogger();
17+
final Logger completionInstallationLogger = _MockLogger();
1818

1919
@override
2020
final CompletionInstallation completionInstallation =
21-
MockCompletionInstallation();
21+
_MockCompletionInstallation();
2222
}
2323

2424
void main() {
@@ -45,6 +45,15 @@ void main() {
4545
});
4646

4747
group('install completion files', () {
48+
test('forces install', () async {
49+
await commandRunner.run(['install-completion-files']);
50+
51+
verify(
52+
() => commandRunner.completionInstallation
53+
.install(commandRunner.executableName, force: true),
54+
).called(1);
55+
});
56+
4857
test('when normal', () async {
4958
await commandRunner.run(['install-completion-files']);
5059

test/src/command_runner/completion_command_runner_test.dart

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:test/test.dart';
88

99
class MockLogger extends Mock implements Logger {}
1010

11-
class MockCompletionInstallation extends Mock
11+
class _MockCompletionInstallation extends Mock
1212
implements CompletionInstallation {}
1313

1414
class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
@@ -116,13 +116,13 @@ void main() {
116116
test('Tries to install completion files on test subcommand', () async {
117117
final commandRunner = _TestCompletionCommandRunner()
118118
..addCommand(_TestUserCommand())
119-
..mockCompletionInstallation = MockCompletionInstallation();
119+
..mockCompletionInstallation = _MockCompletionInstallation();
120120

121121
await commandRunner.run(['ahoy']);
122122

123-
verify(() => commandRunner.completionInstallation.install('test'))
124-
.called(1);
125-
123+
verify(
124+
() => commandRunner.completionInstallation.install('test'),
125+
).called(1);
126126
verify(
127127
() => commandRunner.completionInstallationLogger.level = Level.error,
128128
).called(1);
@@ -132,7 +132,7 @@ void main() {
132132
final commandRunner = _TestCompletionCommandRunner()
133133
..enableAutoInstall = false
134134
..addCommand(_TestUserCommand())
135-
..mockCompletionInstallation = MockCompletionInstallation();
135+
..mockCompletionInstallation = _MockCompletionInstallation();
136136

137137
await commandRunner.run(['ahoy']);
138138

@@ -142,14 +142,32 @@ void main() {
142142
() => commandRunner.completionInstallationLogger.level = any(),
143143
);
144144
});
145+
146+
test('softly tries to install when enabled', () async {
147+
final commandRunner = _TestCompletionCommandRunner()
148+
..enableAutoInstall = true
149+
..addCommand(_TestUserCommand())
150+
..mockCompletionInstallation = _MockCompletionInstallation()
151+
..environmentOverride = {
152+
'SHELL': '/foo/bar/zsh',
153+
};
154+
155+
await commandRunner.run(['ahoy']);
156+
157+
verify(
158+
() => commandRunner.completionInstallation.install(
159+
commandRunner.executableName,
160+
),
161+
).called(1);
162+
});
145163
});
146164

147165
test(
148166
'When it throws CompletionInstallationException, it logs as a warning',
149167
() async {
150168
final commandRunner = _TestCompletionCommandRunner()
151169
..addCommand(_TestUserCommand())
152-
..mockCompletionInstallation = MockCompletionInstallation();
170+
..mockCompletionInstallation = _MockCompletionInstallation();
153171

154172
when(
155173
() => commandRunner.completionInstallation.install('test'),
@@ -168,7 +186,7 @@ void main() {
168186
() async {
169187
final commandRunner = _TestCompletionCommandRunner()
170188
..addCommand(_TestUserCommand())
171-
..mockCompletionInstallation = MockCompletionInstallation();
189+
..mockCompletionInstallation = _MockCompletionInstallation();
172190

173191
when(
174192
() => commandRunner.completionInstallation.install('test'),
@@ -185,7 +203,7 @@ void main() {
185203
'logs a warning wen it throws $CompletionUninstallationException',
186204
() async {
187205
final commandRunner = _TestCompletionCommandRunner()
188-
..mockCompletionInstallation = MockCompletionInstallation();
206+
..mockCompletionInstallation = _MockCompletionInstallation();
189207

190208
when(
191209
() => commandRunner.completionInstallation.uninstall('test'),
@@ -207,7 +225,7 @@ void main() {
207225
'logs an error when an unknown exception happens during a install',
208226
() async {
209227
final commandRunner = _TestCompletionCommandRunner()
210-
..mockCompletionInstallation = MockCompletionInstallation();
228+
..mockCompletionInstallation = _MockCompletionInstallation();
211229

212230
when(
213231
() => commandRunner.completionInstallation.uninstall('test'),

0 commit comments

Comments
 (0)