From 250503f74b3d2697c69d301edc35d146c9f98141 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 9 Sep 2019 08:31:37 +0200 Subject: [PATCH 01/22] select defaultBranch on sources and commits when not yet selected --- scm-ui/src/repos/containers/ChangesetsRoot.js | 21 +++++++++++++++++++ .../src/repos/sources/containers/Sources.js | 11 ++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/repos/containers/ChangesetsRoot.js b/scm-ui/src/repos/containers/ChangesetsRoot.js index 8e0b44afbe..db440b17c0 100644 --- a/scm-ui/src/repos/containers/ChangesetsRoot.js +++ b/scm-ui/src/repos/containers/ChangesetsRoot.js @@ -43,8 +43,29 @@ type Props = { class ChangesetsRoot extends React.Component { componentDidMount() { this.props.fetchBranches(this.props.repository); + this.redirectToDefaultBranch(); } + redirectToDefaultBranch = () => { + if (this.shouldRedirectToDefaultBranch()) { + const defaultBranches = this.props.branches.filter( + b => b.defaultBranch === true + ); + if (defaultBranches.length > 0) { + this.branchSelected(defaultBranches[0]); + } + } + }; + + shouldRedirectToDefaultBranch = () => { + return ( + this.props.branches && + this.props.branches.length > 0 && + this.props.selected !== + this.props.branches.filter(b => b.defaultBranch === true)[0] + ); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 1); diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index d09e18e5be..065dfb5a51 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -80,20 +80,17 @@ class Sources extends React.Component { } redirectToDefaultBranch = () => { - const { branches, baseUrl } = this.props; - if (this.shouldRedirect()) { + const { branches } = this.props; + if (this.shouldRedirectToDefaultBranch()) { const defaultBranches = branches.filter(b => b.defaultBranch); if (defaultBranches.length > 0) { - this.props.history.push( - `${baseUrl}/${encodeURIComponent(defaultBranches[0].name)}/` - ); - this.setState({ selectedBranch: defaultBranches[0] }); + this.branchSelected(defaultBranches[0]); } } }; - shouldRedirect = () => { + shouldRedirectToDefaultBranch = () => { const { branches, revision } = this.props; return branches && !revision; }; From 14451897b2bdfca21e82b1a2bb31852811b4340a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 11:42:26 +0200 Subject: [PATCH 02/22] Introduce PluginDependencyTracker --- .../scm/plugin/PluginDependencyTracker.java | 42 ++++++++++ .../plugin/PluginDependencyTrackerTest.java | 81 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java create mode 100644 scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java new file mode 100644 index 0000000000..bc5cd1dc6d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java @@ -0,0 +1,42 @@ +package sonia.scm.plugin; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; + +class PluginDependencyTracker { + + private final Map> plugins = new HashMap<>(); + + void addInstalled(PluginDescriptor plugin) { + plugin.getDependencies().forEach(dependency -> addDependency(plugin.getInformation().getName(), dependency)); + } + + void removeInstalled(PluginDescriptor plugin) { + doThrow() + .violation("Plugin is needed as a dependency for other plugins", "plugin") + .when(!mayUninstall(plugin.getInformation().getName())); + plugin.getDependencies().forEach(dependency -> removeDependency(plugin.getInformation().getName(), dependency)); + } + + boolean mayUninstall(String name) { + return plugins.computeIfAbsent(name, x -> new HashSet<>()).isEmpty(); + } + + private void addDependency(String from, String to) { + plugins.computeIfAbsent(to, name -> createInitialDependencyCollection(from)); + } + + private void removeDependency(String from, String to) { + plugins.get(to).remove(from); + } + + private Collection createInitialDependencyCollection(String from) { + Collection dependencies = new HashSet<>(); + dependencies.add(from); + return dependencies; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java new file mode 100644 index 0000000000..c38409eb51 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java @@ -0,0 +1,81 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import sonia.scm.ScmConstraintViolationException; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static sonia.scm.plugin.PluginTestHelper.createInstalled; + +class PluginDependencyTrackerTest { + + @Test + void simpleInstalledPluginWithoutDependingPluginsCanBeUninstalled() { + PluginDescriptor mail = createInstalled("scm-mail-plugin").getDescriptor(); + when(mail.getDependencies()).thenReturn(emptySet()); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(mail); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isTrue(); + } + + @Test + void installedPluginWithDependingPluginCannotBeUninstalled() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isFalse(); + } + + @Test + void uninstallOfRequiredPluginShouldThrowException() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + + Assertions.assertThrows( + ScmConstraintViolationException.class, + () -> pluginDependencyTracker.removeInstalled(createInstalled("scm-mail-plugin").getDescriptor()) + ); + } + + @Test + void installedPluginWithDependingPluginCanBeUninstalledAfterDependingPluginIsUninstalled() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + pluginDependencyTracker.removeInstalled(review); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isTrue(); + } + + @Test + void installedPluginWithMultipleDependingPluginCannotBeUninstalledAfterOnlyOneDependingPluginIsUninstalled() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + PluginDescriptor jira = createInstalled("scm-jira-plugin").getDescriptor(); + when(jira.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + pluginDependencyTracker.addInstalled(jira); + pluginDependencyTracker.removeInstalled(jira); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isFalse(); + } +} From 88ed3ff023418efc8933c3c87dc021b5938c584f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 13:22:26 +0200 Subject: [PATCH 03/22] Add uninstall method to plugin manager --- .../java/sonia/scm/plugin/PluginManager.java | 8 +++ .../scm/plugin/DefaultPluginManager.java | 37 ++++++++++++-- .../scm/plugin/DefaultPluginManagerTest.java | 50 +++++++++++++++++-- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index b7b8f69519..f84a975452 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -81,6 +81,14 @@ public interface PluginManager { */ void install(String name, boolean restartAfterInstallation); + /** + * Marks the plugin with the given name for uninstall. + * + * @param name plugin name + * @param restartAfterInstallation restart context after plugin has been marked to really uninstall the plugin + */ + void uninstall(String name, boolean restartAfterInstallation); + /** * Install all pending plugins and restart the scm context. */ diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 807eaac317..11e42891e4 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -33,8 +33,7 @@ package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -43,9 +42,11 @@ import sonia.scm.NotFoundException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; -//~--- JDK imports ------------------------------------------------------------ import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; @@ -54,6 +55,8 @@ import java.util.stream.Collectors; import static sonia.scm.ContextEntry.ContextBuilder.entity; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -67,7 +70,8 @@ public class DefaultPluginManager implements PluginManager { private final PluginLoader loader; private final PluginCenter center; private final PluginInstaller installer; - private final List pendingQueue = new ArrayList<>(); + private final Collection pendingQueue = new ArrayList<>(); + private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); @Inject public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) { @@ -75,6 +79,16 @@ public class DefaultPluginManager implements PluginManager { this.loader = loader; this.center = center; this.installer = installer; + + this.computeRequiredPlugins(); + } + + @VisibleForTesting + synchronized void computeRequiredPlugins() { + loader.getInstalledPlugins() + .stream() + .map(InstalledPlugin::getDescriptor) + .forEach(dependencyTracker::addInstalled); } @Override @@ -153,6 +167,21 @@ public class DefaultPluginManager implements PluginManager { } } + @Override + public void uninstall(String name, boolean restartAfterInstallation) { + PluginPermissions.manage().check(); + InstalledPlugin installed = getInstalled(name) + .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); + + dependencyTracker.removeInstalled(installed.getDescriptor()); + + try { + Files.createFile(installed.getDirectory().resolve("uninstall")); + } catch (IOException e) { + throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); + } + } + @Override public void installPendingAndRestart() { PluginPermissions.manage().check(); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 16e1e2d73a..44799a03fb 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -10,26 +10,37 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; +import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import java.nio.file.Path; import java.util.List; import java.util.Optional; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createInstalled; @ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) class DefaultPluginManagerTest { @Mock @@ -305,6 +316,34 @@ class DefaultPluginManagerTest { assertThat(available.get(0).isPending()).isTrue(); } + @Test + void shouldThrowExceptionWhenUninstallingUnknownPlugin() { + assertThrows(NotFoundException.class, () -> manager.uninstall("no-such-plugin", false)); + } + + @Test + void shouldUseDependencyTrackerForUninstall() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin"); + when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin)); + manager.computeRequiredPlugins(); + + assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false)); + } + + @Test + void shouldCreateUninstallFile(@TempDirectory.TempDir Path temp) { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.getDirectory()).thenReturn(temp); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + manager.uninstall("scm-mail-plugin", false); + + assertThat(temp.resolve("uninstall")).exists(); + } } @Nested @@ -350,6 +389,11 @@ class DefaultPluginManagerTest { assertThrows(AuthorizationException.class, () -> manager.install("test", false)); } + @Test + void shouldThrowAuthorizationExceptionsForUninstallMethod() { + assertThrows(AuthorizationException.class, () -> manager.uninstall("test", false)); + } + @Test void shouldThrowAuthorizationExceptionsForInstallPendingAndRestart() { assertThrows(AuthorizationException.class, () -> manager.installPendingAndRestart()); From 38f05fe689e930453997c71bf5bd686427024887 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:12:49 +0200 Subject: [PATCH 04/22] Prohibit uninstallation of core plugins --- .../java/sonia/scm/plugin/DefaultPluginManager.java | 1 + .../sonia/scm/plugin/DefaultPluginManagerTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 6c715c2842..e646bf6372 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -188,6 +188,7 @@ public class DefaultPluginManager implements PluginManager { PluginPermissions.manage().check(); InstalledPlugin installed = getInstalled(name) .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); + doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore()); dependencyTracker.removeInstalled(installed.getDescriptor()); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 44799a03fb..6e2bb29dba 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -344,6 +344,19 @@ class DefaultPluginManagerTest { assertThat(temp.resolve("uninstall")).exists(); } + + @Test + void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.getDirectory()).thenReturn(temp); + when(mailPlugin.isCore()).thenReturn(true); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false)); + + assertThat(temp.resolve("uninstall")).doesNotExist(); + } } @Nested From 7ec2b0c31d4cbed7928663f8d4c7fcbabcc99edd Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:27:56 +0200 Subject: [PATCH 05/22] Delete plugins marked for uninstall --- .../sonia/scm/plugin/InstalledPlugin.java | 2 ++ .../sonia/scm/lifecycle/PluginBootstrap.java | 27 ++++++++++++++++++- .../scm/plugin/DefaultPluginManager.java | 3 +-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 80ad1da7b8..17eb329c9e 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -45,6 +45,8 @@ import java.nio.file.Path; public final class InstalledPlugin implements Plugin { + public static final String UNINSTALL_MARKER_FILENAME = "uninstall"; + /** * Constructs a new plugin wrapper. * @param descriptor wrapped plugin diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java index ff8c28f51d..549c50f96d 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java @@ -29,6 +29,7 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Path; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -78,12 +79,37 @@ public final class PluginBootstrap { LOG.info("core plugin extraction is disabled"); } + uninstallMarkedPlugins(pluginDirectory.toPath()); return PluginsInternal.collectPlugins(classLoaderLifeCycle, pluginDirectory.toPath()); } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } } + private void uninstallMarkedPlugins(Path pluginDirectory) { + try { + java.nio.file.Files.list(pluginDirectory) + .filter(java.nio.file.Files::isDirectory) + .filter(this::isMarkedForUninstall) + .forEach(this::uninstall); + } catch (IOException e) { + LOG.warn("error occurred while checking for plugins that should be uninstalled", e); + } + } + + private boolean isMarkedForUninstall(Path path) { + return java.nio.file.Files.exists(path.resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); + } + + private void uninstall(Path path) { + try { + LOG.info("deleting plugin directory {}", path); + IOUtil.delete(path.toFile()); + } catch (IOException e) { + LOG.warn("could not delete plugin directory {}", path, e); + } + } + private void renameOldPluginsFolder(File pluginDirectory) { if (new File(pluginDirectory, "classpath.xml").exists()) { File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1"); @@ -96,7 +122,6 @@ public final class PluginBootstrap { } } - private boolean isCorePluginExtractionDisabled() { return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction"); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index e646bf6372..b550c1c00a 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -39,7 +39,6 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; -import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; import sonia.scm.version.Version; @@ -193,7 +192,7 @@ public class DefaultPluginManager implements PluginManager { dependencyTracker.removeInstalled(installed.getDescriptor()); try { - Files.createFile(installed.getDirectory().resolve("uninstall")); + Files.createFile(installed.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); } catch (IOException e) { throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); } From 5a8ed638dde905d698dfdf417136784f00bc9996 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:32:14 +0200 Subject: [PATCH 06/22] Mirror uninstall state in plugin object --- .../main/java/sonia/scm/plugin/InstalledPlugin.java | 10 ++++++++++ .../java/sonia/scm/plugin/DefaultPluginManager.java | 1 + .../sonia/scm/plugin/DefaultPluginManagerTest.java | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 17eb329c9e..219af9763c 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -127,6 +127,14 @@ public final class InstalledPlugin implements Plugin return core; } + public boolean isMarkedForUninstall() { + return markedForUninstall; + } + + public void setMarkedForUninstall(boolean markedForUninstall) { + this.markedForUninstall = markedForUninstall; + } + //~--- fields --------------------------------------------------------------- /** plugin class loader */ @@ -142,4 +150,6 @@ public final class InstalledPlugin implements Plugin private final WebResourceLoader webResourceLoader; private final boolean core; + + private boolean markedForUninstall; } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index b550c1c00a..dd85764bc2 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -190,6 +190,7 @@ public class DefaultPluginManager implements PluginManager { doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore()); dependencyTracker.removeInstalled(installed.getDescriptor()); + installed.setMarkedForUninstall(true); try { Files.createFile(installed.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 6e2bb29dba..7e16e0fa94 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -345,6 +345,18 @@ class DefaultPluginManagerTest { assertThat(temp.resolve("uninstall")).exists(); } + @Test + void shouldMarkPluginForUninstall(@TempDirectory.TempDir Path temp) { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.getDirectory()).thenReturn(temp); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + manager.uninstall("scm-mail-plugin", false); + + verify(mailPlugin).setMarkedForUninstall(true); + } + @Test void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) { InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); From 695f4d58b9a8ddc1d3a85f4228a5033a4724f303 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:43:45 +0200 Subject: [PATCH 07/22] Map uninstall flag --- .../src/main/java/sonia/scm/api/v2/resources/PluginDto.java | 1 + .../main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java | 1 + 2 files changed, 2 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java index d9782643b2..19067b1865 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java @@ -27,6 +27,7 @@ public class PluginDto extends HalRepresentation { private boolean pending; @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean core; + private Boolean markedForUninstall; private Set dependencies; public PluginDto(Links links) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index 222e34832a..e03330ab32 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -83,6 +83,7 @@ public abstract class PluginDtoMapper { }); dto.setCore(plugin.isCore()); + dto.setMarkedForUninstall(plugin.isMarkedForUninstall()); return dto; } From b2294b8ae19d240f3697a167dfef115c4bfcea72 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 15:10:48 +0200 Subject: [PATCH 08/22] Add uninstall endpoint --- .../v2/resources/InstalledPluginResource.java | 19 +++++++++++++++++++ .../scm/api/v2/resources/PluginDtoMapper.java | 7 +++++++ .../scm/api/v2/resources/ResourceLinks.java | 4 ++++ 3 files changed, 30 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java index 5b5d0f267b..f0dff26757 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java @@ -11,9 +11,11 @@ import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.util.List; import java.util.Optional; @@ -80,4 +82,21 @@ public class InstalledPluginResource { throw notFound(entity("Plugin", name)); } } + + /** + * Triggers plugin uninstall. + * @param name plugin name + * @return HTTP Status. + */ + @POST + @Path("/{name}/uninstall") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response uninstallPlugin(@PathParam("name") String name, @QueryParam("restart") boolean restartAfterInstallation) { + PluginPermissions.manage().check(); + pluginManager.uninstall(name, restartAfterInstallation); + return Response.ok().build(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index e03330ab32..e3e1c026ac 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -74,6 +74,13 @@ public abstract class PluginDtoMapper { ) { links.single(link("update", resourceLinks.availablePlugin().install(information.getName()))); } + if (!plugin.isCore() + && (!availablePlugin.isPresent() || !availablePlugin.get().isPending()) + && PluginPermissions.manage().isPermitted() + // TODO check if plugin is no dependency of another plugin + ) { + links.single(link("uninstall", resourceLinks.installedPlugin().uninstall(information.getName()))); + } PluginDto dto = new PluginDto(links.build()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 3a797734ea..4198d8dbd0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -666,6 +666,10 @@ class ResourceLinks { String self(String id) { return installedPluginLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugin").parameters(id).href(); } + + public String uninstall(String name) { + return installedPluginLinkBuilder.method("installedPlugins").parameters().method("uninstallPlugin").parameters(name).href(); + } } public InstalledPluginCollectionLinks installedPluginCollection() { From 0243edf58515627c5b3a80d9976bf26a7ac43571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 16 Sep 2019 17:49:21 +0200 Subject: [PATCH 09/22] Fix tracker --- .../java/sonia/scm/plugin/PluginDependencyTracker.java | 8 +------- .../sonia/scm/plugin/PluginDependencyTrackerTest.java | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java index bc5cd1dc6d..2ec956237a 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java @@ -27,16 +27,10 @@ class PluginDependencyTracker { } private void addDependency(String from, String to) { - plugins.computeIfAbsent(to, name -> createInitialDependencyCollection(from)); + plugins.computeIfAbsent(to, name -> new HashSet<>()).add(from); } private void removeDependency(String from, String to) { plugins.get(to).remove(from); } - - private Collection createInitialDependencyCollection(String from) { - Collection dependencies = new HashSet<>(); - dependencies.add(from); - return dependencies; - } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java index c38409eb51..150d4356e7 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java @@ -73,7 +73,7 @@ class PluginDependencyTrackerTest { PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); pluginDependencyTracker.addInstalled(review); pluginDependencyTracker.addInstalled(jira); - pluginDependencyTracker.removeInstalled(jira); + pluginDependencyTracker.removeInstalled(review); boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); assertThat(mayUninstall).isFalse(); From fc319f90e3d9c27a46bb78c3bc161729ee6a52fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 16 Sep 2019 17:50:05 +0200 Subject: [PATCH 10/22] Append uninstall links --- .../sonia/scm/plugin/InstalledPlugin.java | 12 ++++- .../scm/api/v2/resources/PluginDtoMapper.java | 3 +- .../scm/plugin/DefaultPluginManager.java | 24 +++++++++- .../api/v2/resources/PluginDtoMapperTest.java | 11 +++++ .../scm/plugin/DefaultPluginManagerTest.java | 48 ++++++++++++++++++- 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 219af9763c..5d009a96e4 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -135,7 +135,14 @@ public final class InstalledPlugin implements Plugin this.markedForUninstall = markedForUninstall; } - //~--- fields --------------------------------------------------------------- + public boolean isUninstallable() { + return uninstallable; + } + + public void setUninstallable(boolean uninstallable) { + this.uninstallable = uninstallable; + } +//~--- fields --------------------------------------------------------------- /** plugin class loader */ private final ClassLoader classLoader; @@ -151,5 +158,6 @@ public final class InstalledPlugin implements Plugin private final boolean core; - private boolean markedForUninstall; + private boolean markedForUninstall = false; + private boolean uninstallable = false; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index e3e1c026ac..160ec22354 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -74,10 +74,9 @@ public abstract class PluginDtoMapper { ) { links.single(link("update", resourceLinks.availablePlugin().install(information.getName()))); } - if (!plugin.isCore() + if (plugin.isUninstallable() && (!availablePlugin.isPresent() || !availablePlugin.get().isPending()) && PluginPermissions.manage().isPermitted() - // TODO check if plugin is no dependency of another plugin ) { links.single(link("uninstall", resourceLinks.installedPlugin().uninstall(information.getName()))); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index dd85764bc2..708c9004bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -82,15 +82,16 @@ public class DefaultPluginManager implements PluginManager { this.center = center; this.installer = installer; - this.computeRequiredPlugins(); + this.computeInstallationDependencies(); } @VisibleForTesting - synchronized void computeRequiredPlugins() { + synchronized void computeInstallationDependencies() { loader.getInstalledPlugins() .stream() .map(InstalledPlugin::getDescriptor) .forEach(dependencyTracker::addInstalled); + updateMayUninstallFlag(); } @Override @@ -166,6 +167,7 @@ public class DefaultPluginManager implements PluginManager { for (AvailablePlugin plugin : plugins) { try { PendingPluginInstallation pending = installer.install(plugin); + dependencyTracker.addInstalled(plugin.getDescriptor()); pendingInstallations.add(pending); } catch (PluginInstallException ex) { cancelPending(pendingInstallations); @@ -178,6 +180,7 @@ public class DefaultPluginManager implements PluginManager { restart("plugin installation"); } else { pendingQueue.addAll(pendingInstallations); + updateMayUninstallFlag(); } } } @@ -197,6 +200,23 @@ public class DefaultPluginManager implements PluginManager { } catch (IOException e) { throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); } + + if (restartAfterInstallation) { + restart("plugin installation"); + } else { + updateMayUninstallFlag(); + } + } + + private void updateMayUninstallFlag() { + loader.getInstalledPlugins() + .forEach(p -> p.setUninstallable(isUninstallable(p))); + } + + private boolean isUninstallable(InstalledPlugin p) { + return !p.isCore() + && !p.isMarkedForUninstall() + && dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName()); } @Override diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java index 5bd7c1199e..0da2331bd3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java @@ -127,4 +127,15 @@ class PluginDtoMapperTest { PluginDto dto = mapper.mapAvailable(plugin); assertThat(dto.getDependencies()).containsOnly("one", "two"); } + + @Test + void shouldAppendUninstallLink() { + when(subject.isPermitted("plugin:manage")).thenReturn(true); + InstalledPlugin plugin = createInstalled(createPluginInformation()); + when(plugin.isUninstallable()).thenReturn(true); + + PluginDto dto = mapper.mapInstalled(plugin, emptyList()); + assertThat(dto.getLinks().getLinkBy("uninstall").get().getHref()) + .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin/uninstall"); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 7e16e0fa94..00a8e26c5e 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Optional; +import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -328,7 +329,7 @@ class DefaultPluginManagerTest { when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin)); - manager.computeRequiredPlugins(); + manager.computeInstallationDependencies(); assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false)); } @@ -369,6 +370,51 @@ class DefaultPluginManagerTest { assertThat(temp.resolve("uninstall")).doesNotExist(); } + + @Test + void shouldMarkUninstallablePlugins() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin"); + when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + when(loader.getInstalledPlugins()).thenReturn(asList(mailPlugin, reviewPlugin)); + + manager.computeInstallationDependencies(); + + verify(reviewPlugin).setUninstallable(true); + verify(mailPlugin).setUninstallable(false); + } + + @Test + void shouldUpdateMayUninstallFlagAfterDependencyIsUninstalled() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin"); + when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + when(loader.getInstalledPlugins()).thenReturn(asList(mailPlugin, reviewPlugin)); + + manager.computeInstallationDependencies(); + + manager.uninstall("scm-review-plugin", false); + + verify(mailPlugin).setUninstallable(true); + } + + @Test + void shouldUpdateMayUninstallFlagAfterDependencyIsInstalled() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + AvailablePlugin reviewPlugin = createAvailable("scm-review-plugin"); + when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + when(center.getAvailable()).thenReturn(singleton(reviewPlugin)); + + manager.computeInstallationDependencies(); + + manager.install("scm-review-plugin", false); + + verify(mailPlugin).setUninstallable(false); + } } @Nested From 3e0169b6672b28a3e973a66204e76be002bf9334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 16 Sep 2019 18:00:02 +0200 Subject: [PATCH 11/22] Add uninstalled plugins to pending resource --- .../v2/resources/PendingPluginResource.java | 14 +++++++++++--- .../resources/PendingPluginResourceTest.java | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java index 47e3d2f9f6..14280d5d85 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java @@ -61,16 +61,24 @@ public class PendingPluginResource { Stream updatePlugins = installed .stream() .filter(i -> contains(pending, i)); + Stream uninstallPlugins = installed + .stream() + .filter(InstalledPlugin::isMarkedForUninstall); Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self()); - if (!pending.isEmpty()) { + List installDtos = newPlugins.map(mapper::mapAvailable).collect(toList()); + List updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); + List uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); + + if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) { linksBuilder.single(link("install", resourceLinks.pendingPluginCollection().installPending())); } Embedded.Builder embedded = Embedded.embeddedBuilder(); - embedded.with("new", newPlugins.map(mapper::mapAvailable).collect(toList())); - embedded.with("update", updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList())); + embedded.with("new", installDtos); + embedded.with("update", updateDtos); + embedded.with("uninstall", uninstallDtos); return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build(); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java index d4b8a5bf5a..49f9c30e0b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java @@ -30,6 +30,7 @@ import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import static java.net.URI.create; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -123,7 +124,6 @@ class PendingPluginResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); assertThat(response.getContentAsString()).contains("\"install\":{\"href\":\"/v2/plugins/pending/install\"}"); - System.out.println(response.getContentAsString()); } @Test @@ -140,7 +140,21 @@ class PendingPluginResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\""); assertThat(response.getContentAsString()).contains("\"install\":{\"href\":\"/v2/plugins/pending/install\"}"); - System.out.println(response.getContentAsString()); + } + + @Test + void shouldGetPendingUninstallPluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + when(pluginManager.getAvailable()).thenReturn(emptyList()); + InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); + when(installedPlugin.isMarkedForUninstall()).thenReturn(true); + when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"uninstall\":[{\"name\":\"uninstalled-plugin\""); + assertThat(response.getContentAsString()).contains("\"install\":{\"href\":\"/v2/plugins/pending/install\"}"); } @Test From a6f50e628bf271c116b4df8b0bb2f5d39023fbd7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 17 Sep 2019 13:00:24 +0200 Subject: [PATCH 12/22] fix npe --- .../main/java/sonia/scm/plugin/PluginDependencyTracker.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java index 2ec956237a..a68b391b97 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java @@ -12,7 +12,9 @@ class PluginDependencyTracker { private final Map> plugins = new HashMap<>(); void addInstalled(PluginDescriptor plugin) { - plugin.getDependencies().forEach(dependency -> addDependency(plugin.getInformation().getName(), dependency)); + if (plugin.getDependencies() != null) { + plugin.getDependencies().forEach(dependency -> addDependency(plugin.getInformation().getName(), dependency)); + } } void removeInstalled(PluginDescriptor plugin) { From e10b7df2891ef47ff97fb42c6932a807cc215553 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 17 Sep 2019 16:25:24 +0200 Subject: [PATCH 13/22] implement plugin uninstall modal / add uninstall marked plugins to pending modal --- .../packages/ui-types/src/Plugin.js | 2 + scm-ui/public/locales/de/admin.json | 6 +- scm-ui/public/locales/en/admin.json | 6 +- .../plugins/components/ExecutePendingModal.js | 36 +++-- .../admin/plugins/components/PluginEntry.js | 123 ++++++++++++------ .../admin/plugins/components/PluginModal.js | 81 ++++++------ 6 files changed, 160 insertions(+), 94 deletions(-) diff --git a/scm-ui-components/packages/ui-types/src/Plugin.js b/scm-ui-components/packages/ui-types/src/Plugin.js index 0f9694b5fb..c7612a8bf8 100644 --- a/scm-ui-components/packages/ui-types/src/Plugin.js +++ b/scm-ui-components/packages/ui-types/src/Plugin.js @@ -11,6 +11,7 @@ export type Plugin = { category: string, avatarUrl: string, pending: boolean, + markedForUninstall?: boolean, dependencies: string[], _links: Links }; @@ -31,5 +32,6 @@ export type PendingPlugins = { _embedded: { new: [], update: [], + uninstall: [] } } diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index d2cd2c6640..69b1ed26de 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -34,15 +34,19 @@ "modal": { "title": { "install": "{{name}} Plugin installieren", - "update": "{{name}} Plugin aktualisieren" + "update": "{{name}} Plugin aktualisieren", + "uninstall": "{{name}} Plugin deinstallieren" }, "restart": "Neustarten um Plugin zu aktivieren", "install": "Installieren", "update": "Aktualisieren", + "uninstall": "Deinstallieren", "installQueue": "Werden installiert:", "updateQueue": "Werden aktualisiert:", + "uninstallQueue": "Werden deinstalliert:", "installAndRestart": "Installieren und Neustarten", "updateAndRestart": "Aktualisieren und Neustarten", + "uninstallAndRestart": "Deinstallieren and Neustarten", "executeAndRestart": "Ausführen und Neustarten", "abort": "Abbrechen", "author": "Autor", diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 63f6089b09..47edd1f2d7 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -34,15 +34,19 @@ "modal": { "title": { "install": "Install {{name}} Plugin", - "update": "Update {{name}} Plugin" + "update": "Update {{name}} Plugin", + "uninstall": "Uninstall {{name}} Plugin" }, "restart": "Restart to activate", "install": "Install", "update": "Update", + "uninstall": "Uninstall", "installQueue": "Will be installed:", "updateQueue": "Will be updated:", + "uninstallQueue": "Will be uninstalled:", "installAndRestart": "Install and Restart", "updateAndRestart": "Update and Restart", + "uninstallAndRestart": "Uninstall and Restart", "executeAndRestart": "Execute and Restart", "abort": "Abort", "author": "Author", diff --git a/scm-ui/src/admin/plugins/components/ExecutePendingModal.js b/scm-ui/src/admin/plugins/components/ExecutePendingModal.js index 673ddd18cb..b559413d2b 100644 --- a/scm-ui/src/admin/plugins/components/ExecutePendingModal.js +++ b/scm-ui/src/admin/plugins/components/ExecutePendingModal.js @@ -86,11 +86,9 @@ class ExecutePendingModal extends React.Component { <> {t("plugins.modal.installQueue")}
    - {pendingPlugins._embedded.new - .filter(plugin => plugin.pending) - .map(plugin => ( -
  • {plugin.name}
  • - ))} + {pendingPlugins._embedded.new.map(plugin => ( +
  • {plugin.name}
  • + ))}
)} @@ -107,11 +105,28 @@ class ExecutePendingModal extends React.Component { <> {t("plugins.modal.updateQueue")}
    - {pendingPlugins._embedded.update - .filter(plugin => plugin.pending) - .map(plugin => ( -
  • {plugin.name}
  • - ))} + {pendingPlugins._embedded.update.map(plugin => ( +
  • {plugin.name}
  • + ))} +
+ + )} + + ); + }; + + renderUninstallQueue = () => { + const { pendingPlugins, t } = this.props; + return ( + <> + {pendingPlugins._embedded && + pendingPlugins._embedded.uninstall.length > 0 && ( + <> + {t("plugins.modal.uninstallQueue")} +
    + {pendingPlugins._embedded.uninstall.map(plugin => ( +
  • {plugin.name}
  • + ))}
)} @@ -128,6 +143,7 @@ class ExecutePendingModal extends React.Component {

{t("plugins.modal.executePending")}

{this.renderInstallQueue()} {this.renderUpdateQueue()} + {this.renderUninstallQueue()}
{this.renderNotifications()}
diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js index 0d17af0475..7785c04a35 100644 --- a/scm-ui/src/admin/plugins/components/PluginEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -7,10 +7,10 @@ import PluginAvatar from "./PluginAvatar"; import classNames from "classnames"; import PluginModal from "./PluginModal"; - const PluginAction = { INSTALL: "install", - UPDATE: "update" + UPDATE: "update", + UNINSTALL: "uninstall" }; type Props = { @@ -22,7 +22,9 @@ type Props = { }; type State = { - showModal: boolean + showInstallModal: boolean, + showUpdateModal: boolean, + showUninstallModal: boolean }; const styles = { @@ -45,6 +47,12 @@ const styles = { "& .level": { paddingBottom: "0.5rem" } + }, + actionbar: { + display: "flex", + "& span + span": { + marginLeft: "0.5rem" + } } }; @@ -53,7 +61,9 @@ class PluginEntry extends React.Component { super(props); this.state = { - showModal: false + showInstallModal: false, + showUpdateModal: false, + showUninstallModal: false }; } @@ -61,10 +71,9 @@ class PluginEntry extends React.Component { return ; }; - toggleModal = () => { - this.setState(prevState => ({ - showModal: !prevState.showModal - })); + toggleModal = (showModal: string) => { + const oldValue = this.state[showModal]; + this.setState({ [showModal]: !oldValue }); }; createFooterRight = (plugin: Plugin) => { @@ -81,72 +90,100 @@ class PluginEntry extends React.Component { return plugin._links && plugin._links.update && plugin._links.update.href; }; + isUninstallable = () => { + const { plugin } = this.props; + return ( + plugin._links && plugin._links.uninstall && plugin._links.uninstall.href + ); + }; + createActionbar = () => { const { classes } = this.props; - if (this.isInstallable()) { - return ( - - - - ); - } else if (this.isUpdatable()) { - return ( - - - - ); - } + return ( +
+ {this.isInstallable() && ( + this.toggleModal("showInstallModal")} + > + + + )} + {this.isUninstallable() && ( + this.toggleModal("showUninstallModal")} + > + + + )} + {this.isUpdatable() && ( + this.toggleModal("showUpdateModal")} + > + + + )} +
+ ); }; renderModal = () => { const { plugin, refresh } = this.props; - if (this.isInstallable()) { + if (this.state.showInstallModal && this.isInstallable()) { return ( this.toggleModal("showInstallModal")} /> ); - } else if (this.isUpdatable()) { + } else if (this.state.showUpdateModal && this.isUpdatable()) { return ( this.toggleModal("showUpdateModal")} /> ); + } else if (this.state.showUninstallModal && this.isUninstallable()) { + return ( + this.toggleModal("showUninstallModal")} + /> + ); + } else { + return null; } }; createPendingSpinner = () => { const { plugin, classes } = this.props; - if (plugin.pending) { - return ( - - - - ); - } - return null; + return ( + + + + ); }; render() { const { plugin, classes } = this.props; - const { showModal } = this.state; const avatar = this.createAvatar(plugin); const actionbar = this.createActionbar(); const footerRight = this.createFooterRight(plugin); - const modal = showModal ? this.renderModal() : null; + const modal = this.renderModal(); return ( <> @@ -157,7 +194,9 @@ class PluginEntry extends React.Component { title={plugin.displayName ? plugin.displayName : plugin.name} description={plugin.description} contentRight={ - plugin.pending ? this.createPendingSpinner() : actionbar + plugin.pending || plugin.markedForUninstall + ? this.createPendingSpinner() + : actionbar } footerRight={footerRight} /> diff --git a/scm-ui/src/admin/plugins/components/PluginModal.js b/scm-ui/src/admin/plugins/components/PluginModal.js index 024a9cf32e..0d48fc8ea0 100644 --- a/scm-ui/src/admin/plugins/components/PluginModal.js +++ b/scm-ui/src/admin/plugins/components/PluginModal.js @@ -103,6 +103,8 @@ class PluginModal extends React.Component { pluginActionLink = plugin._links.install.href; } else if (pluginAction === "update") { pluginActionLink = plugin._links.update.href; + } else if (pluginAction === "uninstall") { + pluginActionLink = plugin._links.uninstall.href; } return pluginActionLink + "?restart=" + restart.toString(); }; @@ -256,49 +258,48 @@ class PluginModal extends React.Component { )} - {pluginAction === "update" && ( - <> -
-
- {t("plugins.modal.currentVersion")}: -
-
- {plugin.version} -
+ {(pluginAction === "update" || pluginAction === "uninstall") && ( +
+
+ {t("plugins.modal.currentVersion")}:
-
-
- {t("plugins.modal.newVersion")}: -
-
- {plugin.newVersion} -
+
+ {plugin.version}
- +
+ )} + {pluginAction === "update" && ( +
+
+ {t("plugins.modal.newVersion")}: +
+
+ {plugin.newVersion} +
+
)} - {this.renderDependencies()}
From 780fc2140fc645fcc1b718bd60531a6e15c42ae8 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 17 Sep 2019 16:54:50 +0200 Subject: [PATCH 14/22] fix toggle install modal on click cardColumn --- scm-ui/src/admin/plugins/components/PluginEntry.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js index 7785c04a35..0dca09f94f 100644 --- a/scm-ui/src/admin/plugins/components/PluginEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -189,7 +189,11 @@ class PluginEntry extends React.Component { <> this.toggleModal("showInstallModal") + : null + } avatar={avatar} title={plugin.displayName ? plugin.displayName : plugin.name} description={plugin.description} From f53f6f0a250ae8ccf2e833f000ca1b66db2b279a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 18 Sep 2019 08:35:59 +0200 Subject: [PATCH 15/22] Execute restart, when there are only plugins to uninstall --- .../java/sonia/scm/plugin/DefaultPluginManager.java | 2 +- .../sonia/scm/plugin/DefaultPluginManagerTest.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index b51372e532..e0aa04776f 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -222,7 +222,7 @@ public class DefaultPluginManager implements PluginManager { @Override public void executePendingAndRestart() { PluginPermissions.manage().check(); - if (!pendingQueue.isEmpty()) { + if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) { restart("execute pending plugin changes"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 5ebc500885..057a05eb79 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -415,6 +415,18 @@ class DefaultPluginManagerTest { verify(mailPlugin).setUninstallable(false); } + + @Test + void shouldRestartWithUninstallOnly() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.isMarkedForUninstall()).thenReturn(true); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + manager.executePendingAndRestart(); + + verify(eventBus).post(any(RestartEvent.class)); + } } @Nested From 35c81b508404d108e55711e2dadd84fbf3607100 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Sep 2019 09:30:32 +0200 Subject: [PATCH 16/22] use pluginAction enum instead of strings --- .../src/admin/plugins/components/PluginEntry.js | 2 +- .../src/admin/plugins/components/PluginModal.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js index 0dca09f94f..dbbce06d50 100644 --- a/scm-ui/src/admin/plugins/components/PluginEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -7,7 +7,7 @@ import PluginAvatar from "./PluginAvatar"; import classNames from "classnames"; import PluginModal from "./PluginModal"; -const PluginAction = { +export const PluginAction = { INSTALL: "install", UPDATE: "update", UNINSTALL: "uninstall" diff --git a/scm-ui/src/admin/plugins/components/PluginModal.js b/scm-ui/src/admin/plugins/components/PluginModal.js index 0d48fc8ea0..2ee6196a39 100644 --- a/scm-ui/src/admin/plugins/components/PluginModal.js +++ b/scm-ui/src/admin/plugins/components/PluginModal.js @@ -16,6 +16,7 @@ import { import classNames from "classnames"; import waitForRestart from "./waitForRestart"; import SuccessNotification from "./SuccessNotification"; +import { PluginAction } from "./PluginEntry"; type Props = { plugin: Plugin, @@ -99,11 +100,11 @@ class PluginModal extends React.Component { let pluginActionLink = ""; - if (pluginAction === "install") { + if (pluginAction === PluginAction.INSTALL) { pluginActionLink = plugin._links.install.href; - } else if (pluginAction === "update") { + } else if (pluginAction === PluginAction.UPDATE) { pluginActionLink = plugin._links.update.href; - } else if (pluginAction === "uninstall") { + } else if (pluginAction === PluginAction.UNINSTALL) { pluginActionLink = plugin._links.uninstall.href; } return pluginActionLink + "?restart=" + restart.toString(); @@ -220,7 +221,7 @@ class PluginModal extends React.Component {
{ {plugin.author}
- {pluginAction === "install" && ( + {pluginAction === PluginAction.INSTALL && (
{
)} - {(pluginAction === "update" || pluginAction === "uninstall") && ( + {(pluginAction === PluginAction.UPDATE || + pluginAction === PluginAction.UNINSTALL) && (
{
)} - {pluginAction === "update" && ( + {pluginAction === PluginAction.UPDATE && (
Date: Wed, 18 Sep 2019 08:29:17 +0000 Subject: [PATCH 17/22] Close branch feature/deinstall_plugins From 7e85f374ab7e33272435ef7d143f535552f22c11 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 18 Sep 2019 11:02:43 +0200 Subject: [PATCH 18/22] Close file stream --- .../src/main/java/sonia/scm/lifecycle/PluginBootstrap.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java index 549c50f96d..0382187c26 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java @@ -33,6 +33,7 @@ import java.nio.file.Path; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.stream.Stream; public final class PluginBootstrap { @@ -87,8 +88,8 @@ public final class PluginBootstrap { } private void uninstallMarkedPlugins(Path pluginDirectory) { - try { - java.nio.file.Files.list(pluginDirectory) + try (Stream list = java.nio.file.Files.list(pluginDirectory)) { + list .filter(java.nio.file.Files::isDirectory) .filter(this::isMarkedForUninstall) .forEach(this::uninstall); From 913b7080fdd01c03204819c6cfb691915f66ff10 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Sep 2019 15:47:58 +0200 Subject: [PATCH 19/22] mark installed plugin as core after server started --- .../sonia/scm/plugin/InstalledPlugin.java | 6 +++++- .../scm/plugin/DefaultPluginManager.java | 19 ++++++++++++++----- .../sonia/scm/plugin/PluginProcessor.java | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 5d009a96e4..602378fe6a 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -142,6 +142,10 @@ public final class InstalledPlugin implements Plugin public void setUninstallable(boolean uninstallable) { this.uninstallable = uninstallable; } + + public void markAsCore() { + this.core = true; + } //~--- fields --------------------------------------------------------------- /** plugin class loader */ @@ -156,7 +160,7 @@ public final class InstalledPlugin implements Plugin /** plugin web resource loader */ private final WebResourceLoader webResourceLoader; - private final boolean core; + private boolean core; private boolean markedForUninstall = false; private boolean uninstallable = false; diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index e0aa04776f..a73c2795ed 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -195,11 +195,7 @@ public class DefaultPluginManager implements PluginManager { dependencyTracker.removeInstalled(installed.getDescriptor()); installed.setMarkedForUninstall(true); - try { - Files.createFile(installed.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); - } catch (IOException e) { - throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); - } + createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME); if (restartAfterInstallation) { restart("plugin installation"); @@ -219,6 +215,19 @@ public class DefaultPluginManager implements PluginManager { && dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName()); } + private void markAsCore(InstalledPlugin plugin) { + createMarkerFile(plugin, PluginConstants.FILE_CORE); + plugin.markAsCore(); + } + + private void createMarkerFile(InstalledPlugin plugin, String markerFile) { + try { + Files.createFile(plugin.getDirectory().resolve(markerFile)); + } catch (IOException e) { + throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + markerFile, e); + } + } + @Override public void executePendingAndRestart() { PluginPermissions.manage().check(); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index e1f0367948..2bbab3c650 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -462,7 +462,7 @@ public final class PluginProcessor if (Files.exists(descriptorPath)) { - boolean core = Files.exists(directory.resolve("core")); + boolean core = Files.exists(directory.resolve(PluginConstants.FILE_CORE)); ClassLoader cl = createClassLoader(classLoader, smp); From 60fc7f2889e111db4e1e14b4eb6f9da989dd91f4 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 18 Sep 2019 16:15:29 +0200 Subject: [PATCH 20/22] minor fixes on plugins --- scm-ui/public/locales/de/admin.json | 4 ++-- scm-ui/public/locales/en/admin.json | 2 +- scm-ui/src/admin/plugins/components/PluginModal.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 69b1ed26de..d4f3567059 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -29,7 +29,7 @@ "installedNavLink": "Installiert", "availableNavLink": "Verfügbar" }, - "executePending": "Austehende Plugin-Änderungen ausführen", + "executePending": "Ausstehende Plugin-Änderungen ausführen", "noPlugins": "Keine Plugins gefunden.", "modal": { "title": { @@ -37,7 +37,7 @@ "update": "{{name}} Plugin aktualisieren", "uninstall": "{{name}} Plugin deinstallieren" }, - "restart": "Neustarten um Plugin zu aktivieren", + "restart": "Neustarten, um Plugin-Änderungen wirksam zu machen", "install": "Installieren", "update": "Aktualisieren", "uninstall": "Deinstallieren", diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 47edd1f2d7..c23a88a920 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -37,7 +37,7 @@ "update": "Update {{name}} Plugin", "uninstall": "Uninstall {{name}} Plugin" }, - "restart": "Restart to activate", + "restart": "Restart to make plugin changes effective", "install": "Install", "update": "Update", "uninstall": "Uninstall", diff --git a/scm-ui/src/admin/plugins/components/PluginModal.js b/scm-ui/src/admin/plugins/components/PluginModal.js index 2ee6196a39..b15a068fa2 100644 --- a/scm-ui/src/admin/plugins/components/PluginModal.js +++ b/scm-ui/src/admin/plugins/components/PluginModal.js @@ -45,7 +45,7 @@ const styles = { minWidth: "5.5em" }, userLabelMarginLarge: { - minWidth: "9em" + minWidth: "10em" }, userFieldFlex: { flexGrow: 4 @@ -130,7 +130,7 @@ class PluginModal extends React.Component { const { pluginAction, onClose, t } = this.props; const { loading, error, restart, success } = this.state; - let color = "primary"; + let color = pluginAction === PluginAction.UNINSTALL ? "warning" : "primary"; let label = `plugins.modal.${pluginAction}`; if (restart) { color = "warning"; From 0b3afd197d9269b0ffa92c53c9c3e603ac7c8120 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 19 Sep 2019 06:45:17 +0000 Subject: [PATCH 21/22] Close branch feature/sources_commits_default_branch From 1e5c5d907a18ecdc1016cf4bc195400ff3263402 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 19 Sep 2019 11:21:46 +0200 Subject: [PATCH 22/22] remove unnecessary method mark plugin as core plugin --- scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java | 5 +---- .../src/main/java/sonia/scm/plugin/DefaultPluginManager.java | 5 ----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 602378fe6a..09f05e2c99 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -143,9 +143,6 @@ public final class InstalledPlugin implements Plugin this.uninstallable = uninstallable; } - public void markAsCore() { - this.core = true; - } //~--- fields --------------------------------------------------------------- /** plugin class loader */ @@ -160,7 +157,7 @@ public final class InstalledPlugin implements Plugin /** plugin web resource loader */ private final WebResourceLoader webResourceLoader; - private boolean core; + private final boolean core; private boolean markedForUninstall = false; private boolean uninstallable = false; diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index a73c2795ed..d54645f10d 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -215,11 +215,6 @@ public class DefaultPluginManager implements PluginManager { && dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName()); } - private void markAsCore(InstalledPlugin plugin) { - createMarkerFile(plugin, PluginConstants.FILE_CORE); - plugin.markAsCore(); - } - private void createMarkerFile(InstalledPlugin plugin, String markerFile) { try { Files.createFile(plugin.getDirectory().resolve(markerFile));