mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-07 08:02:09 +01:00
merge with 2.0.0-m3
This commit is contained in:
@@ -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
|
||||
@@ -125,7 +127,23 @@ public final class InstalledPlugin implements Plugin
|
||||
return core;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
public boolean isMarkedForUninstall() {
|
||||
return markedForUninstall;
|
||||
}
|
||||
|
||||
public void setMarkedForUninstall(boolean markedForUninstall) {
|
||||
this.markedForUninstall = markedForUninstall;
|
||||
}
|
||||
|
||||
public boolean isUninstallable() {
|
||||
return uninstallable;
|
||||
}
|
||||
|
||||
public void setUninstallable(boolean uninstallable) {
|
||||
this.uninstallable = uninstallable;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** plugin class loader */
|
||||
private final ClassLoader classLoader;
|
||||
@@ -140,4 +158,7 @@ public final class InstalledPlugin implements Plugin
|
||||
private final WebResourceLoader webResourceLoader;
|
||||
|
||||
private final boolean core;
|
||||
|
||||
private boolean markedForUninstall = false;
|
||||
private boolean uninstallable = false;
|
||||
}
|
||||
|
||||
@@ -81,8 +81,16 @@ 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.
|
||||
*/
|
||||
void installPendingAndRestart();
|
||||
void executePendingAndRestart();
|
||||
}
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,20 +29,24 @@
|
||||
"installedNavLink": "Installiert",
|
||||
"availableNavLink": "Verfügbar"
|
||||
},
|
||||
"executePending": "Austehende Plugin-Änderungen ausführen",
|
||||
"executePending": "Ausstehende Plugin-Änderungen ausführen",
|
||||
"noPlugins": "Keine Plugins gefunden.",
|
||||
"modal": {
|
||||
"title": {
|
||||
"install": "{{name}} Plugin installieren",
|
||||
"update": "{{name}} Plugin aktualisieren"
|
||||
"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",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
"restart": "Restart to make plugin changes effective",
|
||||
"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",
|
||||
|
||||
@@ -86,11 +86,9 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
||||
<>
|
||||
<strong>{t("plugins.modal.installQueue")}</strong>
|
||||
<ul>
|
||||
{pendingPlugins._embedded.new
|
||||
.filter(plugin => plugin.pending)
|
||||
.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
{pendingPlugins._embedded.new.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
@@ -107,11 +105,28 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
||||
<>
|
||||
<strong>{t("plugins.modal.updateQueue")}</strong>
|
||||
<ul>
|
||||
{pendingPlugins._embedded.update
|
||||
.filter(plugin => plugin.pending)
|
||||
.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
{pendingPlugins._embedded.update.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderUninstallQueue = () => {
|
||||
const { pendingPlugins, t } = this.props;
|
||||
return (
|
||||
<>
|
||||
{pendingPlugins._embedded &&
|
||||
pendingPlugins._embedded.uninstall.length > 0 && (
|
||||
<>
|
||||
<strong>{t("plugins.modal.uninstallQueue")}</strong>
|
||||
<ul>
|
||||
{pendingPlugins._embedded.uninstall.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
@@ -128,6 +143,7 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
||||
<p>{t("plugins.modal.executePending")}</p>
|
||||
{this.renderInstallQueue()}
|
||||
{this.renderUpdateQueue()}
|
||||
{this.renderUninstallQueue()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="media">{this.renderNotifications()}</div>
|
||||
|
||||
@@ -7,10 +7,10 @@ import PluginAvatar from "./PluginAvatar";
|
||||
import classNames from "classnames";
|
||||
import PluginModal from "./PluginModal";
|
||||
|
||||
|
||||
const PluginAction = {
|
||||
export 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<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showModal: false
|
||||
showInstallModal: false,
|
||||
showUpdateModal: false,
|
||||
showUninstallModal: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,10 +71,9 @@ class PluginEntry extends React.Component<Props, State> {
|
||||
return <PluginAvatar plugin={plugin} />;
|
||||
};
|
||||
|
||||
toggleModal = () => {
|
||||
this.setState(prevState => ({
|
||||
showModal: !prevState.showModal
|
||||
}));
|
||||
toggleModal = (showModal: string) => {
|
||||
const oldValue = this.state[showModal];
|
||||
this.setState({ [showModal]: !oldValue });
|
||||
};
|
||||
|
||||
createFooterRight = (plugin: Plugin) => {
|
||||
@@ -81,83 +90,117 @@ class PluginEntry extends React.Component<Props, State> {
|
||||
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 (
|
||||
<span
|
||||
className={classNames(classes.link, classes.topRight, "level-item")}
|
||||
onClick={this.toggleModal}
|
||||
>
|
||||
<i className="fas fa-download has-text-info" />
|
||||
</span>
|
||||
);
|
||||
} else if (this.isUpdatable()) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(classes.link, classes.topRight, "level-item")}
|
||||
onClick={this.toggleModal}
|
||||
>
|
||||
<i className="fas fa-sync-alt has-text-info" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classNames(classes.actionbar, classes.topRight)}>
|
||||
{this.isInstallable() && (
|
||||
<span
|
||||
className={classNames(classes.link, "level-item")}
|
||||
onClick={() => this.toggleModal("showInstallModal")}
|
||||
>
|
||||
<i className="fas fa-download has-text-info" />
|
||||
</span>
|
||||
)}
|
||||
{this.isUninstallable() && (
|
||||
<span
|
||||
className={classNames(classes.link, "level-item")}
|
||||
onClick={() => this.toggleModal("showUninstallModal")}
|
||||
>
|
||||
<i className="fas fa-trash has-text-info" />
|
||||
</span>
|
||||
)}
|
||||
{this.isUpdatable() && (
|
||||
<span
|
||||
className={classNames(classes.link, "level-item")}
|
||||
onClick={() => this.toggleModal("showUpdateModal")}
|
||||
>
|
||||
<i className="fas fa-sync-alt has-text-info" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderModal = () => {
|
||||
const { plugin, refresh } = this.props;
|
||||
if (this.isInstallable()) {
|
||||
if (this.state.showInstallModal && this.isInstallable()) {
|
||||
return (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={PluginAction.INSTALL}
|
||||
refresh={refresh}
|
||||
onClose={this.toggleModal}
|
||||
onClose={() => this.toggleModal("showInstallModal")}
|
||||
/>
|
||||
);
|
||||
} else if (this.isUpdatable()) {
|
||||
} else if (this.state.showUpdateModal && this.isUpdatable()) {
|
||||
return (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={PluginAction.UPDATE}
|
||||
refresh={refresh}
|
||||
onClose={this.toggleModal}
|
||||
onClose={() => this.toggleModal("showUpdateModal")}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.showUninstallModal && this.isUninstallable()) {
|
||||
return (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={PluginAction.UNINSTALL}
|
||||
refresh={refresh}
|
||||
onClose={() => this.toggleModal("showUninstallModal")}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
createPendingSpinner = () => {
|
||||
const { plugin, classes } = this.props;
|
||||
if (plugin.pending) {
|
||||
return (
|
||||
<span className={classes.topRight}>
|
||||
<i className="fas fa-spinner fa-spin has-text-info" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<span className={classes.topRight}>
|
||||
<i
|
||||
className={classNames(
|
||||
"fas fa-spinner fa-lg fa-spin",
|
||||
plugin.markedForUninstall ? "has-text-danger" : "has-text-info"
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<CardColumn
|
||||
className={classes.layout}
|
||||
action={this.isInstallable() ? this.toggleModal : null}
|
||||
action={
|
||||
this.isInstallable()
|
||||
? () => this.toggleModal("showInstallModal")
|
||||
: null
|
||||
}
|
||||
avatar={avatar}
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
@@ -44,7 +45,7 @@ const styles = {
|
||||
minWidth: "5.5em"
|
||||
},
|
||||
userLabelMarginLarge: {
|
||||
minWidth: "9em"
|
||||
minWidth: "10em"
|
||||
},
|
||||
userFieldFlex: {
|
||||
flexGrow: 4
|
||||
@@ -99,10 +100,12 @@ class PluginModal extends React.Component<Props, State> {
|
||||
|
||||
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 === PluginAction.UNINSTALL) {
|
||||
pluginActionLink = plugin._links.uninstall.href;
|
||||
}
|
||||
return pluginActionLink + "?restart=" + restart.toString();
|
||||
};
|
||||
@@ -127,7 +130,7 @@ class PluginModal extends React.Component<Props, State> {
|
||||
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";
|
||||
@@ -218,7 +221,7 @@ class PluginModal extends React.Component<Props, State> {
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userLabelAlignment,
|
||||
pluginAction === "install"
|
||||
pluginAction === PluginAction.INSTALL
|
||||
? classes.userLabelMarginSmall
|
||||
: classes.userLabelMarginLarge,
|
||||
"field-label is-inline-flex"
|
||||
@@ -235,7 +238,7 @@ class PluginModal extends React.Component<Props, State> {
|
||||
{plugin.author}
|
||||
</div>
|
||||
</div>
|
||||
{pluginAction === "install" && (
|
||||
{pluginAction === PluginAction.INSTALL && (
|
||||
<div className="field is-horizontal">
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -256,49 +259,49 @@ class PluginModal extends React.Component<Props, State> {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pluginAction === "update" && (
|
||||
<>
|
||||
<div className="field is-horizontal">
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userLabelAlignment,
|
||||
classes.userLabelMarginLarge,
|
||||
"field-label is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{t("plugins.modal.currentVersion")}:
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userFieldFlex,
|
||||
"field-body is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{plugin.version}
|
||||
</div>
|
||||
{(pluginAction === PluginAction.UPDATE ||
|
||||
pluginAction === PluginAction.UNINSTALL) && (
|
||||
<div className="field is-horizontal">
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userLabelAlignment,
|
||||
classes.userLabelMarginLarge,
|
||||
"field-label is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{t("plugins.modal.currentVersion")}:
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userLabelAlignment,
|
||||
classes.userLabelMarginLarge,
|
||||
"field-label is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{t("plugins.modal.newVersion")}:
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userFieldFlex,
|
||||
"field-body is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{plugin.newVersion}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userFieldFlex,
|
||||
"field-body is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{plugin.version}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{pluginAction === PluginAction.UPDATE && (
|
||||
<div className="field is-horizontal">
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userLabelAlignment,
|
||||
classes.userLabelMarginLarge,
|
||||
"field-label is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{t("plugins.modal.newVersion")}:
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.userFieldFlex,
|
||||
"field-body is-inline-flex"
|
||||
)}
|
||||
>
|
||||
{plugin.newVersion}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.renderDependencies()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,29 @@ type Props = {
|
||||
class ChangesetsRoot extends React.Component<Props> {
|
||||
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);
|
||||
|
||||
@@ -80,20 +80,17 @@ class Sources extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,19 +61,24 @@ public class PendingPluginResource {
|
||||
Stream<InstalledPlugin> updatePlugins = installed
|
||||
.stream()
|
||||
.filter(i -> contains(pending, i));
|
||||
Stream<InstalledPlugin> uninstallPlugins = installed
|
||||
.stream()
|
||||
.filter(InstalledPlugin::isMarkedForUninstall);
|
||||
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self());
|
||||
|
||||
List<PluginDto> newPluginDtos = newPlugins.map(mapper::mapAvailable).collect(toList());
|
||||
List<PluginDto> updatePluginDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||
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());
|
||||
|
||||
if (newPluginDtos.size() > 0 || updatePluginDtos.size() > 0) {
|
||||
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().installPending()));
|
||||
if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) {
|
||||
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
||||
}
|
||||
|
||||
Embedded.Builder embedded = Embedded.embeddedBuilder();
|
||||
embedded.with("new", newPluginDtos);
|
||||
embedded.with("update", updatePluginDtos);
|
||||
embedded.with("new", installDtos);
|
||||
embedded.with("update", updateDtos);
|
||||
embedded.with("uninstall", uninstallDtos);
|
||||
|
||||
return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build();
|
||||
}
|
||||
@@ -95,14 +100,14 @@ public class PendingPluginResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/install")
|
||||
@Path("/execute")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response installPending() {
|
||||
public Response executePending() {
|
||||
PluginPermissions.manage().check();
|
||||
pluginManager.installPendingAndRestart();
|
||||
pluginManager.executePendingAndRestart();
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> dependencies;
|
||||
|
||||
public PluginDto(Links links) {
|
||||
|
||||
@@ -74,6 +74,12 @@ public abstract class PluginDtoMapper {
|
||||
) {
|
||||
links.single(link("update", resourceLinks.availablePlugin().install(information.getName())));
|
||||
}
|
||||
if (plugin.isUninstallable()
|
||||
&& (!availablePlugin.isPresent() || !availablePlugin.get().isPending())
|
||||
&& PluginPermissions.manage().isPermitted()
|
||||
) {
|
||||
links.single(link("uninstall", resourceLinks.installedPlugin().uninstall(information.getName())));
|
||||
}
|
||||
|
||||
PluginDto dto = new PluginDto(links.build());
|
||||
|
||||
@@ -83,6 +89,7 @@ public abstract class PluginDtoMapper {
|
||||
});
|
||||
|
||||
dto.setCore(plugin.isCore());
|
||||
dto.setMarkedForUninstall(plugin.isMarkedForUninstall());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -731,8 +735,8 @@ class ResourceLinks {
|
||||
pendingPluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PendingPluginResource.class);
|
||||
}
|
||||
|
||||
String installPending() {
|
||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("installPending").parameters().href();
|
||||
String executePending() {
|
||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href();
|
||||
}
|
||||
|
||||
String self() {
|
||||
|
||||
@@ -29,9 +29,11 @@ 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;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class PluginBootstrap {
|
||||
|
||||
@@ -78,12 +80,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 (Stream<Path> list = java.nio.file.Files.list(pluginDirectory)) {
|
||||
list
|
||||
.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 +123,6 @@ public final class PluginBootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isCorePluginExtractionDisabled() {
|
||||
return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction");
|
||||
}
|
||||
|
||||
@@ -33,21 +33,21 @@
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
//~--- 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;
|
||||
@@ -57,6 +57,8 @@ import java.util.stream.Collectors;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -70,7 +72,8 @@ public class DefaultPluginManager implements PluginManager {
|
||||
private final PluginLoader loader;
|
||||
private final PluginCenter center;
|
||||
private final PluginInstaller installer;
|
||||
private final List<PendingPluginInstallation> pendingQueue = new ArrayList<>();
|
||||
private final Collection<PendingPluginInstallation> pendingQueue = new ArrayList<>();
|
||||
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
|
||||
|
||||
@Inject
|
||||
public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) {
|
||||
@@ -78,6 +81,17 @@ public class DefaultPluginManager implements PluginManager {
|
||||
this.loader = loader;
|
||||
this.center = center;
|
||||
this.installer = installer;
|
||||
|
||||
this.computeInstallationDependencies();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
synchronized void computeInstallationDependencies() {
|
||||
loader.getInstalledPlugins()
|
||||
.stream()
|
||||
.map(InstalledPlugin::getDescriptor)
|
||||
.forEach(dependencyTracker::addInstalled);
|
||||
updateMayUninstallFlag();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,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);
|
||||
@@ -165,15 +180,54 @@ public class DefaultPluginManager implements PluginManager {
|
||||
restart("plugin installation");
|
||||
} else {
|
||||
pendingQueue.addAll(pendingInstallations);
|
||||
updateMayUninstallFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installPendingAndRestart() {
|
||||
public void uninstall(String name, boolean restartAfterInstallation) {
|
||||
PluginPermissions.manage().check();
|
||||
if (!pendingQueue.isEmpty()) {
|
||||
restart("install pending plugins");
|
||||
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());
|
||||
installed.setMarkedForUninstall(true);
|
||||
|
||||
createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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();
|
||||
if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
|
||||
restart("execute pending plugin changes");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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<String, Collection<String>> plugins = new HashMap<>();
|
||||
|
||||
void addInstalled(PluginDescriptor plugin) {
|
||||
if (plugin.getDependencies() != null) {
|
||||
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 -> new HashSet<>()).add(from);
|
||||
}
|
||||
|
||||
private void removeDependency(String from, String to) {
|
||||
plugins.get(to).remove(from);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -122,8 +123,7 @@ class PendingPluginResourceTest {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
|
||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/install\"}");
|
||||
System.out.println(response.getContentAsString());
|
||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,18 +139,32 @@ class PendingPluginResourceTest {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\"");
|
||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/install\"}");
|
||||
System.out.println(response.getContentAsString());
|
||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstallPendingPlugins() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/install");
|
||||
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("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecutePendingPlugins() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
verify(pluginManager).installPendingAndRestart();
|
||||
verify(pluginManager).executePendingAndRestart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,17 +189,17 @@ class PendingPluginResourceTest {
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(pluginManager, never()).installPendingAndRestart();
|
||||
verify(pluginManager, never()).executePendingAndRestart();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotInstallPendingPlugins() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/install");
|
||||
void shouldNotExecutePendingPlugins() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(pluginManager, never()).installPendingAndRestart();
|
||||
verify(pluginManager, never()).executePendingAndRestart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,26 +10,38 @@ 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.Arrays.asList;
|
||||
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
|
||||
@@ -271,14 +283,14 @@ class DefaultPluginManagerTest {
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
|
||||
|
||||
manager.install("scm-review-plugin", false);
|
||||
manager.installPendingAndRestart();
|
||||
manager.executePendingAndRestart();
|
||||
|
||||
verify(eventBus).post(any(RestartEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotSendRestartEventWithoutPendingPlugins() {
|
||||
manager.installPendingAndRestart();
|
||||
manager.executePendingAndRestart();
|
||||
|
||||
verify(eventBus, never()).post(any());
|
||||
}
|
||||
@@ -305,6 +317,116 @@ 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.computeInstallationDependencies();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@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");
|
||||
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();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -351,8 +473,13 @@ class DefaultPluginManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionsForInstallPendingAndRestart() {
|
||||
assertThrows(AuthorizationException.class, () -> manager.installPendingAndRestart());
|
||||
void shouldThrowAuthorizationExceptionsForUninstallMethod() {
|
||||
assertThrows(AuthorizationException.class, () -> manager.uninstall("test", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionsForExecutePendingAndRestart() {
|
||||
assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(review);
|
||||
|
||||
boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin");
|
||||
assertThat(mayUninstall).isFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user