mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-06 06:39:15 +01:00
Enable plugin management via CLI (#2087)
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
@@ -32,8 +32,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import sonia.scm.lifecycle.Restarter;
|
||||
import sonia.scm.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PendingPlugins;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.plugin.PluginPermissions;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -44,9 +43,7 @@ import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
@@ -94,28 +91,13 @@ public class PendingPluginResource {
|
||||
)
|
||||
)
|
||||
public Response getPending() {
|
||||
List<AvailablePlugin> pending = pluginManager
|
||||
.getAvailable()
|
||||
.stream()
|
||||
.filter(AvailablePlugin::isPending)
|
||||
.collect(toList());
|
||||
List<InstalledPlugin> installed = pluginManager.getInstalled();
|
||||
|
||||
Stream<AvailablePlugin> newPlugins = pending
|
||||
.stream()
|
||||
.filter(a -> !contains(installed, a));
|
||||
Stream<InstalledPlugin> updatePlugins = installed
|
||||
.stream()
|
||||
.filter(i -> contains(pending, i));
|
||||
Stream<InstalledPlugin> uninstallPlugins = installed
|
||||
.stream()
|
||||
.filter(InstalledPlugin::isMarkedForUninstall);
|
||||
PendingPlugins pending = pluginManager.getPending();
|
||||
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self());
|
||||
|
||||
List<PluginDto> installDtos = newPlugins.map(mapper::mapAvailable).collect(toList());
|
||||
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||
List<PluginDto> installDtos = pending.getInstall().stream().map(mapper::mapAvailable).collect(toList());
|
||||
List<PluginDto> updateDtos = pending.getUpdate().stream().map(p -> mapper.mapInstalled(p, pending.getInstall())).collect(toList());
|
||||
List<PluginDto> uninstallDtos = pending.getUninstall().stream().map(i -> mapper.mapInstalled(i, pending.getInstall())).collect(toList());
|
||||
|
||||
if (
|
||||
PluginPermissions.write().isPermitted() &&
|
||||
@@ -135,22 +117,6 @@ public class PendingPluginResource {
|
||||
return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build();
|
||||
}
|
||||
|
||||
private boolean contains(Collection<InstalledPlugin> installedPlugins, AvailablePlugin availablePlugin) {
|
||||
return installedPlugins
|
||||
.stream()
|
||||
.anyMatch(installedPlugin -> haveSameName(installedPlugin, availablePlugin));
|
||||
}
|
||||
|
||||
private boolean contains(Collection<AvailablePlugin> availablePlugins, InstalledPlugin installedPlugin) {
|
||||
return availablePlugins
|
||||
.stream()
|
||||
.anyMatch(availablePlugin -> haveSameName(installedPlugin, availablePlugin));
|
||||
}
|
||||
|
||||
private boolean haveSameName(InstalledPlugin installedPlugin, AvailablePlugin availablePlugin) {
|
||||
return installedPlugin.getDescriptor().getInformation().getName().equals(availablePlugin.getDescriptor().getInformation().getName());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/execute")
|
||||
@Operation(
|
||||
|
||||
@@ -41,6 +41,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
@@ -87,15 +88,11 @@ public class DefaultPluginManager implements PluginManager {
|
||||
this.eventBus = eventBus;
|
||||
this.pluginSetConfigStore = pluginSetConfigStore;
|
||||
|
||||
if (contextFactory != null) {
|
||||
this.contextFactory = contextFactory;
|
||||
} else {
|
||||
this.contextFactory = (plugins -> {
|
||||
List<AvailablePlugin> pendingPlugins = new ArrayList<>(plugins);
|
||||
pendingInstallQueue.stream().map(PendingPluginInstallation::getPlugin).forEach(pendingPlugins::add);
|
||||
return PluginInstallationContext.from(getInstalled(), pendingPlugins);
|
||||
});
|
||||
}
|
||||
this.contextFactory = Objects.requireNonNullElseGet(contextFactory, () -> (plugins -> {
|
||||
List<AvailablePlugin> pendingPlugins = new ArrayList<>(plugins);
|
||||
pendingInstallQueue.stream().map(PendingPluginInstallation::getPlugin).forEach(pendingPlugins::add);
|
||||
return PluginInstallationContext.from(getInstalled(), pendingPlugins);
|
||||
}));
|
||||
|
||||
this.computeInstallationDependencies();
|
||||
}
|
||||
@@ -192,6 +189,7 @@ public class DefaultPluginManager implements PluginManager {
|
||||
|
||||
@Override
|
||||
public List<InstalledPlugin> getUpdatable() {
|
||||
PluginPermissions.read().check();
|
||||
return getInstalled()
|
||||
.stream()
|
||||
.filter(p -> isUpdatable(p.getDescriptor().getInformation().getName()))
|
||||
@@ -199,6 +197,12 @@ public class DefaultPluginManager implements PluginManager {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingPlugins getPending() {
|
||||
PluginPermissions.read().check();
|
||||
return new PendingPlugins(getAvailable(), getInstalled());
|
||||
}
|
||||
|
||||
private <T extends Plugin> Predicate<T> filterByName(String name) {
|
||||
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
|
||||
}
|
||||
@@ -361,7 +365,7 @@ public class DefaultPluginManager implements PluginManager {
|
||||
}
|
||||
|
||||
private boolean isUpdatable(String name) {
|
||||
return getAvailable(name).isPresent() && !getPending(name).isPresent();
|
||||
return getAvailable(name).isPresent() && getPending(name).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "add")
|
||||
class PluginAddCommand implements Runnable {
|
||||
|
||||
@CommandLine.Parameters(index = "0", paramLabel = "<name>", descriptionKey = "scm.plugin.name")
|
||||
private String name;
|
||||
|
||||
@CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply")
|
||||
private boolean apply;
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final PluginTemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Inject
|
||||
PluginAddCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (manager.getInstalled(name).isPresent()) {
|
||||
templateRenderer.renderPluginAlreadyInstalledError();
|
||||
return;
|
||||
}
|
||||
if (manager.getAvailable(name).isEmpty()) {
|
||||
templateRenderer.renderPluginNotAvailableError();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
manager.install(name, apply);
|
||||
} catch (Exception e) {
|
||||
templateRenderer.renderPluginCouldNotBeAdded(name);
|
||||
throw e;
|
||||
}
|
||||
templateRenderer.renderPluginAdded(name);
|
||||
if (!apply) {
|
||||
templateRenderer.renderServerRestartRequired();
|
||||
} else {
|
||||
templateRenderer.renderServerRestartTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setApply(boolean apply) {
|
||||
this.apply = apply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "apply")
|
||||
class PluginApplyCommand implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = {"--yes", "-y"}, descriptionKey = "scm.plugin.restart")
|
||||
private boolean restart;
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final PluginTemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Inject
|
||||
PluginApplyCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!restart) {
|
||||
templateRenderer.renderConfirmServerRestart();
|
||||
return;
|
||||
}
|
||||
if (manager.getPending().existPendingChanges()) {
|
||||
manager.executePendingAndRestart();
|
||||
templateRenderer.renderServerRestartTriggered();
|
||||
} else {
|
||||
templateRenderer.renderSkipServerRestart();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setRestart(boolean restart) {
|
||||
this.restart = restart;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(name = "plugin")
|
||||
public class PluginCommand {
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.cli.Table;
|
||||
import sonia.scm.cli.TemplateRenderer;
|
||||
import sonia.scm.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PendingPlugins;
|
||||
import sonia.scm.plugin.PluginDescriptor;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "list", aliases = "ls")
|
||||
class PluginListCommand implements Runnable {
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final TemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@CommandLine.Option(names = {"--short", "-s"})
|
||||
private boolean useShortTemplate;
|
||||
|
||||
private static final String TABLE_TEMPLATE = String.join("\n",
|
||||
"{{#rows}}",
|
||||
"{{#cols}}{{#row.first}}{{#upper}}{{value}}{{/upper}}{{/row.first}}{{^row.first}}{{value}}{{/row.first}}{{^last}} {{/last}}{{/cols}}",
|
||||
"{{/rows}}"
|
||||
);
|
||||
|
||||
private static final String SHORT_TEMPLATE = String.join("\n",
|
||||
"{{#plugins}}",
|
||||
"{{name}}",
|
||||
"{{/plugins}}"
|
||||
);
|
||||
|
||||
@Inject
|
||||
public PluginListCommand(TemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Collection<ListablePlugin> plugins = getListablePlugins();
|
||||
if (useShortTemplate) {
|
||||
templateRenderer.renderToStdout(SHORT_TEMPLATE, Map.of("plugins", plugins));
|
||||
} else {
|
||||
Table table = templateRenderer.createTable();
|
||||
String yes = spec.resourceBundle().getString("yes");
|
||||
table.addHeader("scm.plugin.name", "scm.plugin.displayName", "scm.plugin.availableVersion", "scm.plugin.installedVersion", "scm.plugin.pending");
|
||||
|
||||
for (ListablePlugin plugin : plugins) {
|
||||
table.addRow(
|
||||
plugin.getName(),
|
||||
plugin.getDisplayName(),
|
||||
plugin.getAvailableVersion(),
|
||||
plugin.getInstalledVersion(),
|
||||
plugin.isPending() ? yes : ""
|
||||
);
|
||||
}
|
||||
templateRenderer.renderToStdout(TABLE_TEMPLATE, Map.of("rows", table, "plugins", plugins));
|
||||
}
|
||||
}
|
||||
|
||||
private List<ListablePlugin> getListablePlugins() {
|
||||
List<InstalledPlugin> installedPlugins = manager.getInstalled();
|
||||
List<AvailablePlugin> availablePlugins = manager.getAvailable();
|
||||
PendingPlugins pendingPlugins = manager.getPending();
|
||||
|
||||
Set<ListablePlugin> plugins = new HashSet<>();
|
||||
for (PluginDescriptor pluginDesc : installedPlugins.stream().map(InstalledPlugin::getDescriptor).collect(Collectors.toList())) {
|
||||
ListablePlugin listablePlugin = new ListablePlugin(pendingPlugins, pluginDesc, true);
|
||||
setAvailableVersion(listablePlugin);
|
||||
plugins.add(listablePlugin);
|
||||
}
|
||||
|
||||
for (PluginDescriptor pluginDesc : availablePlugins.stream().map(AvailablePlugin::getDescriptor).collect(Collectors.toList())) {
|
||||
if (plugins.stream().noneMatch(p -> p.name.equals(pluginDesc.getInformation().getName()))) {
|
||||
plugins.add(new ListablePlugin(pendingPlugins, pluginDesc, false));
|
||||
}
|
||||
}
|
||||
|
||||
return plugins.stream().sorted((a, b) -> a.name.compareToIgnoreCase(b.name)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void setAvailableVersion(ListablePlugin listablePlugin) {
|
||||
Optional<AvailablePlugin> availablePlugin = manager.getAvailable().stream().filter(p -> p.getDescriptor().getInformation().getName().equals(listablePlugin.name)).findFirst();
|
||||
if (availablePlugin.isPresent() && !availablePlugin.get().getDescriptor().getInformation().getVersion().equals(listablePlugin.installedVersion)) {
|
||||
listablePlugin.setAvailableVersion(availablePlugin.get().getDescriptor().getInformation().getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setSpec(CommandLine.Model.CommandSpec spec) {
|
||||
this.spec = spec;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setUseShortTemplate(boolean useShortTemplate) {
|
||||
this.useShortTemplate = useShortTemplate;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
static class ListablePlugin {
|
||||
private String name;
|
||||
private String displayName;
|
||||
private String installedVersion;
|
||||
private String availableVersion;
|
||||
private boolean pending;
|
||||
private boolean installed;
|
||||
|
||||
ListablePlugin(PendingPlugins pendingPlugins, PluginDescriptor descriptor, boolean installed) {
|
||||
this.name = descriptor.getInformation().getName();
|
||||
this.displayName = descriptor.getInformation().getDisplayName();
|
||||
if (installed) {
|
||||
this.installedVersion = descriptor.getInformation().getVersion();
|
||||
} else {
|
||||
this.availableVersion = descriptor.getInformation().getVersion();
|
||||
}
|
||||
this.pending = pendingPlugins.isPending(descriptor.getInformation().getName());
|
||||
this.installed = installed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "remove", aliases = "rm")
|
||||
class PluginRemoveCommand implements Runnable {
|
||||
|
||||
@CommandLine.Parameters(index = "0", paramLabel = "<name>", descriptionKey = "scm.plugin.name")
|
||||
private String name;
|
||||
|
||||
@CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply")
|
||||
private boolean apply;
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final PluginTemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Inject
|
||||
PluginRemoveCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (manager.getInstalled(name).isEmpty()) {
|
||||
templateRenderer.renderPluginNotInstalledError();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
manager.uninstall(name, apply);
|
||||
} catch (Exception e) {
|
||||
templateRenderer.renderPluginCouldNotBeRemoved(name);
|
||||
throw e;
|
||||
}
|
||||
templateRenderer.renderPluginRemoved(name);
|
||||
if (!apply) {
|
||||
templateRenderer.renderServerRestartRequired();
|
||||
} else {
|
||||
templateRenderer.renderServerRestartTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setApply(boolean apply) {
|
||||
this.apply = apply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "cancel-pending", aliases = "reset")
|
||||
class PluginResetChangesCommand implements Runnable {
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final PluginTemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Inject
|
||||
PluginResetChangesCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (manager.getPending().existPendingChanges()) {
|
||||
manager.cancelPending();
|
||||
templateRenderer.renderPluginsReseted();
|
||||
} else {
|
||||
templateRenderer.renderNoPendingPlugins();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import sonia.scm.cli.CliContext;
|
||||
import sonia.scm.cli.ExitCode;
|
||||
import sonia.scm.cli.TemplateRenderer;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
class PluginTemplateRenderer extends TemplateRenderer {
|
||||
|
||||
private static final String PLUGIN_NOT_AVAILABLE_ERROR_TEMPLATE = "{{i18n.scmPluginNotAvailable}}";
|
||||
private static final String PLUGIN_NOT_INSTALLED_ERROR_TEMPLATE = "{{i18n.scmPluginNotInstalled}}";
|
||||
private static final String PLUGIN_ALREADY_INSTALLED_ERROR_TEMPLATE = "{{i18n.scmPluginAlreadyInstalled}}";
|
||||
private static final String PLUGIN_NOT_REMOVED_ERROR_TEMPLATE = "{{i18n.scmPluginNotRemoved}}";
|
||||
private static final String PLUGIN_NOT_ADDED_ERROR_TEMPLATE = "{{i18n.scmPluginNotAdded}}";
|
||||
private static final String PLUGIN_NOT_UPDATABLE_ERROR_TEMPLATE = "{{i18n.scmPluginNotUpdatable}}";
|
||||
private static final String PLUGIN_UPDATE_ERROR_TEMPLATE = "{{i18n.scmPluginsUpdateFailed}}";
|
||||
private static final String PLUGIN_ADDED_TEMPLATE = "{{i18n.scmPluginAdded}}";
|
||||
private static final String PLUGIN_REMOVED_TEMPLATE = "{{i18n.scmPluginRemoved}}";
|
||||
private static final String PLUGIN_UPDATED_TEMPLATE = "{{i18n.scmPluginUpdated}}";
|
||||
private static final String ALL_PLUGINS_UPDATED_TEMPLATE = "{{i18n.scmPluginsUpdated}}";
|
||||
private static final String SERVER_RESTART_REQUIRED_TEMPLATE = "{{i18n.scmServerRestartRequired}}";
|
||||
private static final String SERVER_RESTART_TRIGGERED_TEMPLATE = "{{i18n.scmServerRestartTriggered}}";
|
||||
private static final String SERVER_RESTART_SKIPPED_TEMPLATE = "{{i18n.scmServerRestartSkipped}}";
|
||||
private static final String SERVER_RESTART_CONFIRMATION_TEMPLATE = "{{i18n.scmServerRestartConfirmation}}";
|
||||
private static final String PLUGINS_NOT_PENDING_ERROR_TEMPLATE = "{{i18n.scmPluginsNotPending}}";
|
||||
private static final String ALL_PENDING_PLUGINS_CANCELLED = "{{i18n.scmPendingPluginsCancelled}}";
|
||||
|
||||
private static final String PLUGIN = "plugin";
|
||||
private final CliContext context;
|
||||
|
||||
@Inject
|
||||
PluginTemplateRenderer(CliContext context, TemplateEngineFactory templateEngineFactory) {
|
||||
super(context, templateEngineFactory);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void renderPluginAdded(String pluginName) {
|
||||
renderToStdout(PLUGIN_ADDED_TEMPLATE, Map.of(PLUGIN, pluginName));
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderPluginRemoved(String pluginName) {
|
||||
renderToStdout(PLUGIN_REMOVED_TEMPLATE, Map.of(PLUGIN, pluginName));
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderPluginUpdated(String pluginName) {
|
||||
renderToStdout(PLUGIN_UPDATED_TEMPLATE, Map.of(PLUGIN, pluginName));
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderAllPluginsUpdated() {
|
||||
renderToStdout(ALL_PLUGINS_UPDATED_TEMPLATE, Collections.emptyMap());
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderPluginCouldNotBeRemoved(String pluginName) {
|
||||
renderToStderr(PLUGIN_NOT_REMOVED_ERROR_TEMPLATE, Map.of(PLUGIN, pluginName));
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.USAGE);
|
||||
}
|
||||
|
||||
public void renderPluginCouldNotBeAdded(String pluginName) {
|
||||
renderToStderr(PLUGIN_NOT_ADDED_ERROR_TEMPLATE, Map.of(PLUGIN, pluginName));
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.SERVER_ERROR);
|
||||
}
|
||||
|
||||
public void renderPluginNotUpdatable(String pluginName) {
|
||||
renderToStderr(PLUGIN_NOT_UPDATABLE_ERROR_TEMPLATE, Map.of(PLUGIN, pluginName));
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.USAGE);
|
||||
}
|
||||
|
||||
public void renderServerRestartRequired() {
|
||||
renderToStdout(SERVER_RESTART_REQUIRED_TEMPLATE, Collections.emptyMap());
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderServerRestartTriggered() {
|
||||
renderToStdout(SERVER_RESTART_TRIGGERED_TEMPLATE, Collections.emptyMap());
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderSkipServerRestart() {
|
||||
renderToStdout(SERVER_RESTART_SKIPPED_TEMPLATE, Collections.emptyMap());
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderPluginsReseted() {
|
||||
renderToStdout(ALL_PENDING_PLUGINS_CANCELLED, Collections.emptyMap());
|
||||
context.getStdout().println();
|
||||
}
|
||||
|
||||
public void renderNoPendingPlugins() {
|
||||
renderToStderr(PLUGINS_NOT_PENDING_ERROR_TEMPLATE, Collections.emptyMap());
|
||||
context.getStderr().println();
|
||||
}
|
||||
|
||||
public void renderConfirmServerRestart() {
|
||||
renderToStderr(SERVER_RESTART_CONFIRMATION_TEMPLATE, Collections.emptyMap());
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.USAGE);
|
||||
}
|
||||
|
||||
public void renderPluginNotAvailableError() {
|
||||
renderToStderr(PLUGIN_NOT_AVAILABLE_ERROR_TEMPLATE, Collections.emptyMap());
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.USAGE);
|
||||
}
|
||||
|
||||
public void renderPluginsUpdateError() {
|
||||
renderToStderr(PLUGIN_UPDATE_ERROR_TEMPLATE, Collections.emptyMap());
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.SERVER_ERROR);
|
||||
}
|
||||
|
||||
public void renderPluginNotInstalledError() {
|
||||
renderToStderr(PLUGIN_NOT_INSTALLED_ERROR_TEMPLATE, Collections.emptyMap());
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.USAGE);
|
||||
}
|
||||
|
||||
public void renderPluginAlreadyInstalledError() {
|
||||
renderToStderr(PLUGIN_ALREADY_INSTALLED_ERROR_TEMPLATE, Collections.emptyMap());
|
||||
context.getStderr().println();
|
||||
context.exit(ExitCode.USAGE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "update-all")
|
||||
class PluginUpdateAllCommand implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply")
|
||||
private boolean apply;
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final PluginTemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Inject
|
||||
PluginUpdateAllCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
manager.updateAll();
|
||||
} catch (Exception e) {
|
||||
templateRenderer.renderPluginsUpdateError();
|
||||
throw e;
|
||||
}
|
||||
templateRenderer.renderAllPluginsUpdated();
|
||||
if (!apply) {
|
||||
templateRenderer.renderServerRestartRequired();
|
||||
} else {
|
||||
manager.executePendingAndRestart();
|
||||
templateRenderer.renderServerRestartTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setApply(boolean apply) {
|
||||
this.apply = apply;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.plugin.cli;
|
||||
|
||||
import com.cronutils.utils.VisibleForTesting;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.cli.ParentCommand;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Objects;
|
||||
|
||||
@ParentCommand(value = PluginCommand.class)
|
||||
@CommandLine.Command(name = "update")
|
||||
class PluginUpdateCommand implements Runnable {
|
||||
|
||||
@CommandLine.Parameters(index = "0", paramLabel = "<name>", descriptionKey = "scm.plugin.name")
|
||||
private String name;
|
||||
|
||||
@CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply")
|
||||
private boolean apply;
|
||||
|
||||
@CommandLine.Mixin
|
||||
private final PluginTemplateRenderer templateRenderer;
|
||||
private final PluginManager manager;
|
||||
@CommandLine.Spec
|
||||
private CommandLine.Model.CommandSpec spec;
|
||||
|
||||
@Inject
|
||||
PluginUpdateCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) {
|
||||
this.templateRenderer = templateRenderer;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (manager.getInstalled(name).isEmpty()) {
|
||||
templateRenderer.renderPluginNotInstalledError();
|
||||
return;
|
||||
}
|
||||
if (manager.getUpdatable().stream().noneMatch(p -> Objects.equals(p.getDescriptor().getInformation().getName(), name))) {
|
||||
templateRenderer.renderPluginNotUpdatable(name);
|
||||
return;
|
||||
}
|
||||
manager.install(name, apply);
|
||||
templateRenderer.renderPluginUpdated(name);
|
||||
if (!apply) {
|
||||
templateRenderer.renderServerRestartRequired();
|
||||
} else {
|
||||
templateRenderer.renderServerRestartTriggered();
|
||||
}
|
||||
}
|
||||
@VisibleForTesting
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
@VisibleForTesting
|
||||
void setApply(boolean apply) {
|
||||
this.apply = apply;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user