Merge with 2.0.0-m3

This commit is contained in:
Rene Pfeuffer
2019-10-02 11:48:37 +02:00
60 changed files with 1105 additions and 410 deletions

View File

@@ -73,6 +73,13 @@ public interface PluginManager {
*/ */
List<AvailablePlugin> getAvailable(); List<AvailablePlugin> getAvailable();
/**
* Returns all updatable plugins.
*
* @return a list of updatable plugins.
*/
List<InstalledPlugin> getUpdatable();
/** /**
* Installs the plugin with the given name from the list of available plugins. * Installs the plugin with the given name from the list of available plugins.
* *
@@ -93,4 +100,14 @@ public interface PluginManager {
* Install all pending plugins and restart the scm context. * Install all pending plugins and restart the scm context.
*/ */
void executePendingAndRestart(); void executePendingAndRestart();
/**
* Cancel all pending plugins.
*/
void cancelPending();
/**
* Update all installed plugins.
*/
void updateAll();
} }

View File

@@ -0,0 +1,22 @@
package sonia.scm.repository;
import sonia.scm.BadRequestException;
import static java.util.Collections.emptyList;
@SuppressWarnings("squid:MaximumInheritanceDepth")
public class NoCommonHistoryException extends BadRequestException {
public NoCommonHistoryException() {
this("no common history");
}
public NoCommonHistoryException(String message) {
super(emptyList(), message);
}
@Override
public String getCode() {
return "4iRct4avG1";
}
}

View File

@@ -40,7 +40,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.spi.DiffCommand; import sonia.scm.repository.spi.DiffCommand;
import sonia.scm.util.IOUtil;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -103,16 +102,12 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
* Passes the difference of the given parameter to the outputstream. * Passes the difference of the given parameter to the outputstream.
* *
* *
* @param outputStream outputstream for the difference * @return A consumer that expects the output stream for the difference
*
* @return {@code this}
* *
* @throws IOException * @throws IOException
*/ */
public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException { public OutputStreamConsumer retrieveContent() throws IOException {
getDiffResult(outputStream); return getDiffResult();
return this;
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -125,21 +120,10 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
* @throws IOException * @throws IOException
*/ */
public String getContent() throws IOException { public String getContent() throws IOException {
String content = null; try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ByteArrayOutputStream baos = null; getDiffResult();
return baos.toString();
try
{
baos = new ByteArrayOutputStream();
getDiffResult(baos);
content = baos.toString();
} }
finally
{
IOUtil.close(baos);
}
return content;
} }
//~--- set methods ---------------------------------------------------------- //~--- set methods ----------------------------------------------------------
@@ -169,25 +153,25 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
* Method description * Method description
* *
* *
* @param outputStream
*
* @throws IOException * @throws IOException
* @return
*/ */
private void getDiffResult(OutputStream outputStream) throws IOException { private OutputStreamConsumer getDiffResult() throws IOException {
Preconditions.checkNotNull(outputStream, "OutputStream is required");
Preconditions.checkArgument(request.isValid(), Preconditions.checkArgument(request.isValid(),
"path and/or revision is required"); "path and/or revision is required");
if (logger.isDebugEnabled())
{
logger.debug("create diff for {}", request); logger.debug("create diff for {}", request);
}
diffCommand.getDiffResult(request, outputStream); return diffCommand.getDiffResult(request);
} }
@Override @Override
DiffCommandBuilder self() { DiffCommandBuilder self() {
return this; return this;
} }
@FunctionalInterface
public interface OutputStreamConsumer {
void accept(OutputStream outputStream) throws IOException;
}
} }

View File

@@ -33,8 +33,9 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import sonia.scm.repository.api.DiffCommandBuilder;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
/** /**
* *
@@ -49,10 +50,9 @@ public interface DiffCommand
* *
* *
* @param request * @param request
* @param output
*
* @throws IOException * @throws IOException
* @throws RuntimeException * @throws RuntimeException
* @return
*/ */
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException; DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException;
} }

View File

@@ -42,7 +42,10 @@ import com.google.common.collect.Multimap;
import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.attributes.Attribute;
import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lfs.LfsPointer;
import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
@@ -55,6 +58,7 @@ import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.LfsFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
@@ -65,10 +69,12 @@ import sonia.scm.web.GitUserAgentProvider;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static java.util.Optional.empty;
import static java.util.Optional.of; import static java.util.Optional.of;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -727,7 +733,26 @@ public final class GitUtil
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE); mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1)); mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1));
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2)); mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2));
return mergeBaseWalk.next().getId(); RevCommit ancestor = mergeBaseWalk.next();
if (ancestor == null) {
String msg = "revisions %s and %s are not related and therefore do not have a common ancestor";
throw new NoCommonHistoryException(String.format(msg, revision1.name(), revision2.name()));
}
return ancestor.getId();
}
}
public static Optional<LfsPointer> getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) throws IOException {
Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
Attribute filter = attributes.get("filter");
if (filter != null && "lfs".equals(filter.getValue())) {
ObjectId blobId = treeWalk.getObjectId(0);
try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) {
return of(LfsPointer.parseLfsPointer(is));
}
} else {
return empty();
} }
} }

View File

@@ -55,7 +55,7 @@ final class Differ implements AutoCloseable {
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) if (!Strings.isNullOrEmpty(request.getAncestorChangeset()))
{ {
ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); ObjectId otherRevision = repository.resolve(request.getAncestorChangeset());
ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision); ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision);
RevTree tree = walk.parseCommit(ancestorId).getTree(); RevTree tree = walk.parseCommit(ancestorId).getTree();
treeWalk.addTree(tree); treeWalk.addTree(tree);
} }
@@ -82,10 +82,6 @@ final class Differ implements AutoCloseable {
return new Differ(commit, walk, treeWalk); return new Differ(commit, walk, treeWalk);
} }
private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
return GitUtil.computeCommonAncestor(repository, revision1, revision2);
}
private Diff diff() throws IOException { private Diff diff() throws IOException {
List<DiffEntry> entries = DiffEntry.scan(treeWalk); List<DiffEntry> entries = DiffEntry.scan(treeWalk);
return new Diff(commit, entries); return new Diff(commit, entries);
@@ -115,4 +111,5 @@ final class Differ implements AutoCloseable {
return entries; return entries;
} }
} }
} }

View File

@@ -38,6 +38,7 @@ package sonia.scm.repository.spi;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import org.eclipse.jgit.lfs.LfsPointer;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
@@ -57,13 +58,17 @@ import sonia.scm.repository.GitSubModuleParser;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.SubRepository; import sonia.scm.repository.SubRepository;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound; import static sonia.scm.NotFoundException.notFound;
@@ -86,18 +91,20 @@ public class GitBrowseCommand extends AbstractGitCommand
*/ */
private static final Logger logger = private static final Logger logger =
LoggerFactory.getLogger(GitBrowseCommand.class); LoggerFactory.getLogger(GitBrowseCommand.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
/** /**
* Constructs ... * Constructs ...
*
* @param context * @param context
* @param repository * @param repository
* @param lfsBlobStoreFactory
*/ */
public GitBrowseCommand(GitContext context, Repository repository) public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory)
{ {
super(context, repository); super(context, repository);
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -195,7 +202,6 @@ public class GitBrowseCommand extends AbstractGitCommand
ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
file.setDirectory(loader.getType() == Constants.OBJ_TREE); file.setDirectory(loader.getType() == Constants.OBJ_TREE);
file.setLength(loader.getSize());
// don't show message and date for directories to improve performance // don't show message and date for directories to improve performance
if (!file.isDirectory() &&!request.isDisableLastCommit()) if (!file.isDirectory() &&!request.isDisableLastCommit())
@@ -203,6 +209,16 @@ public class GitBrowseCommand extends AbstractGitCommand
logger.trace("fetch last commit for {} at {}", path, revId.getName()); logger.trace("fetch last commit for {} at {}", path, revId.getName());
RevCommit commit = getLatestCommit(repo, revId, path); RevCommit commit = getLatestCommit(repo, revId, path);
Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, commit, treeWalk);
if (lfsPointer.isPresent()) {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
Blob blob = lfsBlobStore.get(lfsPointer.get().getOid().getName());
file.setLength(blob.getSize());
} else {
file.setLength(loader.getSize());
}
if (commit != null) if (commit != null)
{ {
file.setLastModified(GitUtil.getCommitTime(commit)); file.setLastModified(GitUtil.getCommitTime(commit));
@@ -375,7 +391,7 @@ public class GitBrowseCommand extends AbstractGitCommand
Map<String, SubRepository> subRepositories; Map<String, SubRepository> subRepositories;
try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() )
{ {
new GitCatCommand(context, repository).getContent(repo, revision, new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision,
PATH_MODULES, baos); PATH_MODULES, baos);
subRepositories = GitSubModuleParser.parse(baos.toString()); subRepositories = GitSubModuleParser.parse(baos.toString());
} }

View File

@@ -33,6 +33,7 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lfs.LfsPointer;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
@@ -45,13 +46,18 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.Closeable; import java.io.Closeable;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Optional;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound; import static sonia.scm.NotFoundException.notFound;
@@ -61,15 +67,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class); private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class);
public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository) { private final LfsBlobStoreFactory lfsBlobStoreFactory;
public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) {
super(context, repository); super(context, repository);
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
} }
@Override @Override
public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException { public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException {
logger.debug("try to read content for {}", request); logger.debug("try to read content for {}", request);
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) { try (Loader closableObjectLoaderContainer = getLoader(request)) {
closableObjectLoaderContainer.objectLoader.copyTo(output); closableObjectLoaderContainer.copyTo(output);
} }
} }
@@ -80,18 +89,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
} }
void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException { void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException {
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) { try (Loader closableObjectLoaderContainer = getLoader(repo, revId, path)) {
closableObjectLoaderContainer.objectLoader.copyTo(output); closableObjectLoaderContainer.copyTo(output);
} }
} }
private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException { private Loader getLoader(CatCommandRequest request) throws IOException {
org.eclipse.jgit.lib.Repository repo = open(); org.eclipse.jgit.lib.Repository repo = open();
ObjectId revId = getCommitOrDefault(repo, request.getRevision()); ObjectId revId = getCommitOrDefault(repo, request.getRevision());
return getLoader(repo, revId, request.getPath()); return getLoader(repo, revId, request.getPath());
} }
private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException { private Loader getLoader(Repository repo, ObjectId revId, String path) throws IOException {
TreeWalk treeWalk = new TreeWalk(repo); TreeWalk treeWalk = new TreeWalk(repo);
treeWalk.setRecursive(Util.nonNull(path).contains("/")); treeWalk.setRecursive(Util.nonNull(path).contains("/"));
@@ -116,21 +125,67 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
treeWalk.setFilter(PathFilter.create(path)); treeWalk.setFilter(PathFilter.create(path));
if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
ObjectId blobId = treeWalk.getObjectId(0); Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, entry, treeWalk);
ObjectLoader loader = repo.open(blobId); if (lfsPointer.isPresent()) {
return loadFromLfsStore(treeWalk, revWalk, lfsPointer.get());
return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk); } else {
return loadFromGit(repo, treeWalk, revWalk);
}
} else { } else {
throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository)); throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository));
} }
} }
private static class ClosableObjectLoaderContainer implements Closeable { private Loader loadFromGit(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException {
ObjectId blobId = treeWalk.getObjectId(0);
ObjectLoader loader = repo.open(blobId);
return new GitObjectLoaderWrapper(loader, treeWalk, revWalk);
}
private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
Blob blob = lfsBlobStore.get(lfsPointer.getOid().getName());
GitUtil.release(revWalk);
GitUtil.release(treeWalk);
return new BlobLoader(blob);
}
private interface Loader extends Closeable {
void copyTo(OutputStream output) throws IOException;
InputStream openStream() throws IOException;
}
private static class BlobLoader implements Loader {
private final InputStream inputStream;
private BlobLoader(Blob blob) throws IOException {
this.inputStream = blob.getInputStream();
}
@Override
public void copyTo(OutputStream output) throws IOException {
IOUtil.copy(inputStream, output);
}
@Override
public InputStream openStream() {
return inputStream;
}
@Override
public void close() throws IOException {
this.inputStream.close();
}
}
private static class GitObjectLoaderWrapper implements Loader {
private final ObjectLoader objectLoader; private final ObjectLoader objectLoader;
private final TreeWalk treeWalk; private final TreeWalk treeWalk;
private final RevWalk revWalk; private final RevWalk revWalk;
private ClosableObjectLoaderContainer(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { private GitObjectLoaderWrapper(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) {
this.objectLoader = objectLoader; this.objectLoader = objectLoader;
this.treeWalk = treeWalk; this.treeWalk = treeWalk;
this.revWalk = revWalk; this.revWalk = revWalk;
@@ -141,14 +196,22 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
GitUtil.release(revWalk); GitUtil.release(revWalk);
GitUtil.release(treeWalk); GitUtil.release(treeWalk);
} }
public void copyTo(OutputStream output) throws IOException {
this.objectLoader.copyTo(output);
}
public InputStream openStream() throws IOException {
return objectLoader.openStream();
}
} }
private static class InputStreamWrapper extends FilterInputStream { private static class InputStreamWrapper extends FilterInputStream {
private final ClosableObjectLoaderContainer container; private final Loader container;
private InputStreamWrapper(ClosableObjectLoaderContainer container) throws IOException { private InputStreamWrapper(Loader container) throws IOException {
super(container.objectLoader.openStream()); super(container.openStream());
this.container = container; this.container = container;
} }

View File

@@ -31,15 +31,12 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.DiffFormatter;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffCommandBuilder;
import java.io.BufferedOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
/** /**
* *
@@ -52,14 +49,16 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
} }
@Override @Override
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException { public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException {
@SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService
org.eclipse.jgit.lib.Repository repository = open(); org.eclipse.jgit.lib.Repository repository = open();
try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) {
formatter.setRepository(repository);
Differ.Diff diff = Differ.diff(repository, request); Differ.Diff diff = Differ.diff(repository, request);
return output -> {
try (DiffFormatter formatter = new DiffFormatter(output)) {
formatter.setRepository(repository);
for (DiffEntry e : diff.getEntries()) { for (DiffEntry e : diff.getEntries()) {
if (!e.getOldId().equals(e.getNewId())) { if (!e.getOldId().equals(e.getNewId())) {
formatter.format(e); formatter.format(e);
@@ -68,6 +67,7 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
formatter.flush(); formatter.flush();
} }
};
} }
} }

View File

@@ -39,6 +39,7 @@ import sonia.scm.repository.Feature;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.IOException; import java.io.IOException;
import java.util.EnumSet; import java.util.EnumSet;
@@ -76,9 +77,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider) { public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) {
this.handler = handler; this.handler = handler;
this.repository = repository; this.repository = repository;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
} }
@@ -143,7 +145,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override @Override
public BrowseCommand getBrowseCommand() public BrowseCommand getBrowseCommand()
{ {
return new GitBrowseCommand(context, repository); return new GitBrowseCommand(context, repository, lfsBlobStoreFactory);
} }
/** /**
@@ -155,7 +157,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override @Override
public CatCommand getCatCommand() public CatCommand getCatCommand()
{ {
return new GitCatCommand(context, repository); return new GitCatCommand(context, repository, lfsBlobStoreFactory);
} }
/** /**
@@ -281,11 +283,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private GitContext context; private final GitContext context;
/** Field description */ /** Field description */
private GitRepositoryHandler handler; private final GitRepositoryHandler handler;
/** Field description */ /** Field description */
private Repository repository; private final Repository repository;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
} }

View File

@@ -39,6 +39,7 @@ import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/** /**
* *
@@ -49,11 +50,13 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
private final GitRepositoryHandler handler; private final GitRepositoryHandler handler;
private final GitRepositoryConfigStoreProvider storeProvider; private final GitRepositoryConfigStoreProvider storeProvider;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject @Inject
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) { public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) {
this.handler = handler; this.handler = handler;
this.storeProvider = storeProvider; this.storeProvider = storeProvider;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
} }
@Override @Override
@@ -61,7 +64,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
GitRepositoryServiceProvider provider = null; GitRepositoryServiceProvider provider = null;
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider); provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory);
} }
return provider; return provider;

View File

@@ -171,6 +171,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
} }
private GitBrowseCommand createCommand() { private GitBrowseCommand createCommand() {
return new GitBrowseCommand(createContext(), repository); return new GitBrowseCommand(createContext(), repository, null);
} }
} }

View File

@@ -39,12 +39,18 @@ import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** /**
* Unit tests for {@link GitCatCommand}. * Unit tests for {@link GitCatCommand}.
@@ -136,7 +142,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
CatCommandRequest request = new CatCommandRequest(); CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt"); request.setPath("b.txt");
InputStream catResultStream = new GitCatCommand(createContext(), repository).getCatResultStream(request); InputStream catResultStream = new GitCatCommand(createContext(), repository, null).getCatResultStream(request);
assertEquals('b', catResultStream.read()); assertEquals('b', catResultStream.read());
assertEquals('\n', catResultStream.read()); assertEquals('\n', catResultStream.read());
@@ -145,13 +151,38 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
catResultStream.close(); catResultStream.close();
} }
@Test
public void testLfsStream() throws IOException {
LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
BlobStore blobStore = mock(BlobStore.class);
Blob blob = mock(Blob.class);
when(lfsBlobStoreFactory.getLfsBlobStore(repository)).thenReturn(blobStore);
when(blobStore.get("d2252bd9fde1bb2ae7531b432c48262c3cbe4df4376008986980de40a7c9cf8b"))
.thenReturn(blob);
when(blob.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[]{'i', 's'}));
CatCommandRequest request = new CatCommandRequest();
request.setRevision("lfs-test");
request.setPath("lfs-image.png");
InputStream catResultStream = new GitCatCommand(createContext(), repository, lfsBlobStoreFactory)
.getCatResultStream(request);
assertEquals('i', catResultStream.read());
assertEquals('s', catResultStream.read());
assertEquals(-1, catResultStream.read());
catResultStream.close();
}
private String execute(CatCommandRequest request) throws IOException { private String execute(CatCommandRequest request) throws IOException {
String content = null; String content = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
try try
{ {
new GitCatCommand(createContext(), repository).getCatResult(request, new GitCatCommand(createContext(), repository, null).getCatResult(request,
baos); baos);
} }
finally finally

View File

@@ -44,7 +44,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest, output); gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
} }
@@ -54,7 +54,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
diffCommandRequest.setRevision("test-branch"); diffCommandRequest.setRevision("test-branch");
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest, output); gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
} }
@@ -65,7 +65,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
diffCommandRequest.setRevision("test-branch"); diffCommandRequest.setRevision("test-branch");
diffCommandRequest.setPath("a.txt"); diffCommandRequest.setPath("a.txt");
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest, output); gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_FILE_A, output.toString()); assertEquals(DIFF_FILE_A, output.toString());
} }
@@ -76,7 +76,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
diffCommandRequest.setRevision("master"); diffCommandRequest.setRevision("master");
diffCommandRequest.setAncestorChangeset("test-branch"); diffCommandRequest.setAncestorChangeset("test-branch");
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest, output); gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString()); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString());
} }
@@ -88,7 +88,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
diffCommandRequest.setAncestorChangeset("test-branch"); diffCommandRequest.setAncestorChangeset("test-branch");
diffCommandRequest.setPath("a.txt"); diffCommandRequest.setPath("a.txt");
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
gitDiffCommand.getDiffResult(diffCommandRequest, output); gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString());
} }
} }

View File

@@ -39,13 +39,12 @@ import com.google.common.base.Strings;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables; import com.google.common.io.Closeables;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.spi.javahg.HgDiffInternalCommand; import sonia.scm.repository.spi.javahg.HgDiffInternalCommand;
import sonia.scm.web.HgUtil; import sonia.scm.web.HgUtil;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -71,9 +70,9 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@Override @Override
public void getDiffResult(DiffCommandRequest request, OutputStream output) public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request)
throws IOException
{ {
return output -> {
com.aragost.javahg.Repository hgRepo = open(); com.aragost.javahg.Repository hgRepo = open();
HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo);
@@ -88,24 +87,19 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand
InputStream inputStream = null; InputStream inputStream = null;
try try {
{
if (!Strings.isNullOrEmpty(request.getPath())) if (!Strings.isNullOrEmpty(request.getPath())) {
{
inputStream = cmd.stream(hgRepo.file(request.getPath())); inputStream = cmd.stream(hgRepo.file(request.getPath()));
} } else {
else
{
inputStream = cmd.stream(); inputStream = cmd.stream();
} }
ByteStreams.copy(inputStream, output); ByteStreams.copy(inputStream, output);
} } finally {
finally
{
Closeables.close(inputStream, true); Closeables.close(inputStream, true);
} }
};
} }
} }

View File

@@ -46,11 +46,10 @@ import org.tmatesoft.svn.core.wc.SVNRevision;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.SvnUtil; import sonia.scm.repository.SvnUtil;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.DiffFormat;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import java.io.OutputStream;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
@@ -70,12 +69,12 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand {
} }
@Override @Override
public void getDiffResult(DiffCommandRequest request, OutputStream output) { public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) {
logger.debug("create diff for {}", request); logger.debug("create diff for {}", request);
Preconditions.checkNotNull(request, "request is required"); Preconditions.checkNotNull(request, "request is required");
Preconditions.checkNotNull(output, "outputstream is required");
String path = request.getPath(); String path = request.getPath();
return output -> {
SVNClientManager clientManager = null; SVNClientManager clientManager = null;
try { try {
SVNURL svnurl = context.createUrl(); SVNURL svnurl = context.createUrl();
@@ -98,5 +97,6 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand {
} finally { } finally {
SvnUtil.dispose(clientManager); SvnUtil.dispose(clientManager);
} }
};
} }
} }

View File

@@ -46,7 +46,7 @@ export function createUrl(url: string) {
class ApiClient { class ApiClient {
get(url: string): Promise<Response> { get(url: string): Promise<Response> {
return fetch(createUrl(url), applyFetchOptions).then(handleFailure); return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
} }
post(url: string, payload: any, contentType: string = "application/json") { post(url: string, payload: any, contentType: string = "application/json") {

View File

@@ -14,7 +14,7 @@ class ButtonGroup extends React.Component<Props> {
const childWrapper = []; const childWrapper = [];
React.Children.forEach(children, child => { React.Children.forEach(children, child => {
if (child) { if (child) {
childWrapper.push(<p className="control" key={childWrapper.length}>{child}</p>); childWrapper.push(<div className="control" key={childWrapper.length}>{child}</div>);
} }
}); });

View File

@@ -0,0 +1,21 @@
//@flow
import * as React from "react";
import classNames from "classnames";
type Props = {
className?: string,
left?: React.Node,
right?: React.Node
};
export default class Level extends React.Component<Props> {
render() {
const { className, left, right } = this.props;
return (
<div className={classNames("level", className)}>
<div className="level-left">{left}</div>
<div className="level-right">{right}</div>
</div>
);
}
}

View File

@@ -2,6 +2,7 @@
export { default as Footer } from "./Footer.js"; export { default as Footer } from "./Footer.js";
export { default as Header } from "./Header.js"; export { default as Header } from "./Header.js";
export { default as Level } from "./Level.js";
export { default as Page } from "./Page.js"; export { default as Page } from "./Page.js";
export { default as PageActions } from "./PageActions.js"; export { default as PageActions } from "./PageActions.js";
export { default as Subtitle } from "./Subtitle.js"; export { default as Subtitle } from "./Subtitle.js";

View File

@@ -4,7 +4,8 @@ import DiffFile from "./DiffFile";
import type {DiffObjectProps, File} from "./DiffTypes"; import type {DiffObjectProps, File} from "./DiffTypes";
type Props = DiffObjectProps & { type Props = DiffObjectProps & {
diff: File[] diff: File[],
defaultCollapse?: boolean
}; };
class Diff extends React.Component<Props> { class Diff extends React.Component<Props> {
@@ -17,7 +18,7 @@ class Diff extends React.Component<Props> {
return ( return (
<> <>
{diff.map((file, index) => ( {diff.map((file, index) => (
<DiffFile key={index} file={file} {...fileProps} /> <DiffFile key={index} file={file} {...fileProps} {...this.props} />
))} ))}
</> </>
); );

View File

@@ -13,6 +13,7 @@ import classNames from "classnames";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Button, ButtonGroup } from "../buttons"; import { Button, ButtonGroup } from "../buttons";
import Tag from "../Tag"; import Tag from "../Tag";
import Icon from "../Icon";
const styles = { const styles = {
panel: { panel: {
@@ -69,7 +70,7 @@ const styles = {
type Props = DiffObjectProps & { type Props = DiffObjectProps & {
file: File, file: File,
collapsible: true, defaultCollapse: boolean,
// context props // context props
classes: any, classes: any,
@@ -82,16 +83,31 @@ type State = {
}; };
class DiffFile extends React.Component<Props, State> { class DiffFile extends React.Component<Props, State> {
static defaultProps = {
defaultCollapse: false
};
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
collapsed: false, collapsed: this.props.defaultCollapse,
sideBySide: false sideBySide: false
}; };
} }
// collapse diff by clicking collapseDiffs button
componentDidUpdate(prevProps) {
const { defaultCollapse } = this.props;
if (prevProps.defaultCollapse !== defaultCollapse) {
this.setState({
collapsed: defaultCollapse
});
}
}
toggleCollapse = () => { toggleCollapse = () => {
if (this.props.collapsable) { const { file } = this.props;
if (file && !file.isBinaray) {
this.setState(state => ({ this.setState(state => ({
collapsed: !state.collapsed collapsed: !state.collapsed
})); }));
@@ -173,7 +189,8 @@ class DiffFile extends React.Component<Props, State> {
) { ) {
return ( return (
<> <>
{file.oldPath} <i className="fa fa-arrow-right" /> {file.newPath} {file.oldPath} <Icon name="arrow-right" color="inherit" />{" "}
{file.newPath}
</> </>
); );
} else if (file.type === "delete") { } else if (file.type === "delete") {
@@ -233,7 +250,6 @@ class DiffFile extends React.Component<Props, State> {
file, file,
fileControlFactory, fileControlFactory,
fileAnnotationFactory, fileAnnotationFactory,
collapsible,
classes, classes,
t t
} = this.props; } = this.props;
@@ -241,12 +257,12 @@ class DiffFile extends React.Component<Props, State> {
const viewType = sideBySide ? "split" : "unified"; const viewType = sideBySide ? "split" : "unified";
let body = null; let body = null;
let icon = "fa fa-angle-right"; let icon = "angle-right";
if (!collapsed) { if (!collapsed) {
const fileAnnotations = fileAnnotationFactory const fileAnnotations = fileAnnotationFactory
? fileAnnotationFactory(file) ? fileAnnotationFactory(file)
: null; : null;
icon = "fa fa-angle-down"; icon = "angle-down";
body = ( body = (
<div className="panel-block is-paddingless"> <div className="panel-block is-paddingless">
{fileAnnotations} {fileAnnotations}
@@ -259,7 +275,9 @@ class DiffFile extends React.Component<Props, State> {
</div> </div>
); );
} }
const collapseIcon = collapsible ? <i className={icon} /> : null; const collapseIcon = !file.isBinary ? (
<Icon name={icon} color="inherit" />
) : null;
const fileControls = fileControlFactory const fileControls = fileControlFactory
? fileControlFactory(file, this.setCollapse) ? fileControlFactory(file, this.setCollapse)
@@ -285,20 +303,10 @@ class DiffFile extends React.Component<Props, State> {
<ButtonGroup> <ButtonGroup>
<Button <Button
action={this.toggleSideBySide} action={this.toggleSideBySide}
className="reduced-mobile" icon={sideBySide ? "align-left" : "columns"}
> label={t(sideBySide ? "diff.combined" : "diff.sideBySide")}
<span className="icon is-small"> reducedMobile={true}
<i
className={classNames(
"fas",
sideBySide ? "fa-align-left" : "fa-columns"
)}
/> />
</span>
<span>
{t(sideBySide ? "diff.combined" : "diff.sideBySide")}
</span>
</Button>
{fileControls} {fileControls}
</ButtonGroup> </ButtonGroup>
</div> </div>

View File

@@ -9,7 +9,8 @@ import Diff from "./Diff";
import type {DiffObjectProps, File} from "./DiffTypes"; import type {DiffObjectProps, File} from "./DiffTypes";
type Props = DiffObjectProps & { type Props = DiffObjectProps & {
url: string url: string,
defaultCollapse?: boolean
}; };
type State = { type State = {

View File

@@ -7,6 +7,7 @@ import {translate} from "react-i18next";
type Props = { type Props = {
changeset: Changeset, changeset: Changeset,
defaultCollapse?: boolean,
// context props // context props
t: string => string t: string => string
@@ -23,12 +24,12 @@ class ChangesetDiff extends React.Component<Props> {
} }
render() { render() {
const { changeset, t } = this.props; const { changeset, defaultCollapse, t } = this.props;
if (!this.isDiffSupported(changeset)) { if (!this.isDiffSupported(changeset)) {
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>; return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
} else { } else {
const url = this.createUrl(changeset); const url = this.createUrl(changeset);
return <LoadingDiff url={url} />; return <LoadingDiff url={url} defaultCollapse={defaultCollapse} />;
} }
} }

View File

@@ -17,6 +17,7 @@ export type Plugin = {
}; };
export type PluginCollection = Collection & { export type PluginCollection = Collection & {
_links: Links,
_embedded: { _embedded: {
plugins: Plugin[] | string[] plugins: Plugin[] | string[]
} }

View File

@@ -29,7 +29,11 @@
"installedNavLink": "Installiert", "installedNavLink": "Installiert",
"availableNavLink": "Verfügbar" "availableNavLink": "Verfügbar"
}, },
"executePending": "Ausstehende Plugin-Änderungen ausführen", "executePending": "Änderungen ausführen",
"outdatedPlugins": "{{count}} veraltetes Plugin",
"outdatedPlugins_plural": "{{count}} veraltete Plugins",
"updateAll": "Alle Plugins aktualisieren",
"cancelPending": "Änderungen abbrechen",
"noPlugins": "Keine Plugins gefunden.", "noPlugins": "Keine Plugins gefunden.",
"modal": { "modal": {
"title": { "title": {
@@ -48,6 +52,7 @@
"updateAndRestart": "Aktualisieren und Neustarten", "updateAndRestart": "Aktualisieren und Neustarten",
"uninstallAndRestart": "Deinstallieren and Neustarten", "uninstallAndRestart": "Deinstallieren and Neustarten",
"executeAndRestart": "Ausführen und Neustarten", "executeAndRestart": "Ausführen und Neustarten",
"updateAll": "Alle Plugins aktualisieren",
"abort": "Abbrechen", "abort": "Abbrechen",
"author": "Autor", "author": "Autor",
"version": "Version", "version": "Version",
@@ -58,7 +63,9 @@
"successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:", "successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
"reload": "jetzt neu laden", "reload": "jetzt neu laden",
"restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", "restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.",
"executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet." "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet.",
"cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.",
"updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam."
} }
}, },
"repositoryRole": { "repositoryRole": {

View File

@@ -73,7 +73,8 @@
"errorTitle": "Fehler", "errorTitle": "Fehler",
"errorSubtitle": "Changesets konnten nicht abgerufen werden", "errorSubtitle": "Changesets konnten nicht abgerufen werden",
"noChangesets": "Keine Changesets in diesem Branch gefunden.", "noChangesets": "Keine Changesets in diesem Branch gefunden.",
"branchSelectorLabel": "Branches" "branchSelectorLabel": "Branches",
"collapseDiffs": "Auf-/Zuklappen"
}, },
"changeset": { "changeset": {
"description": "Beschreibung", "description": "Beschreibung",

View File

@@ -29,7 +29,11 @@
"installedNavLink": "Installed", "installedNavLink": "Installed",
"availableNavLink": "Available" "availableNavLink": "Available"
}, },
"executePending": "Execute pending plugin changes", "executePending": "Execute changes",
"outdatedPlugins": "{{count}} outdated plugin",
"outdatedPlugins_plural": "{{count}} outdated plugins",
"updateAll": "Update all plugins",
"cancelPending": "Cancel changes",
"noPlugins": "No plugins found.", "noPlugins": "No plugins found.",
"modal": { "modal": {
"title": { "title": {
@@ -48,6 +52,7 @@
"updateAndRestart": "Update and Restart", "updateAndRestart": "Update and Restart",
"uninstallAndRestart": "Uninstall and Restart", "uninstallAndRestart": "Uninstall and Restart",
"executeAndRestart": "Execute and Restart", "executeAndRestart": "Execute and Restart",
"updateAll": "Update all plugins",
"abort": "Abort", "abort": "Abort",
"author": "Author", "author": "Author",
"version": "Version", "version": "Version",
@@ -58,7 +63,9 @@
"successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:", "successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:",
"reload": "reload now", "reload": "reload now",
"restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.", "restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.",
"executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted." "executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted.",
"cancelPending": "The following plugin changes will be canceled.",
"updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective."
} }
}, },
"repositoryRole": { "repositoryRole": {

View File

@@ -73,7 +73,8 @@
"errorTitle": "Error", "errorTitle": "Error",
"errorSubtitle": "Could not fetch changesets", "errorSubtitle": "Could not fetch changesets",
"noChangesets": "No changesets found for this branch.", "noChangesets": "No changesets found for this branch.",
"branchSelectorLabel": "Branches" "branchSelectorLabel": "Branches",
"collapseDiffs": "Collapse"
}, },
"changeset": { "changeset": {
"description": "Description", "description": "Description",

View File

@@ -30,6 +30,8 @@
"availableNavLink": "Disponibles" "availableNavLink": "Disponibles"
}, },
"executePending": "Ejecutar los complementos pendientes", "executePending": "Ejecutar los complementos pendientes",
"updateAll": "Actualizar todos los complementos",
"cancelPending": "Cancelar los complementos pendientes",
"noPlugins": "No se han encontrado complementos.", "noPlugins": "No se han encontrado complementos.",
"modal": { "modal": {
"title": { "title": {

View File

@@ -73,7 +73,8 @@
"errorTitle": "Error", "errorTitle": "Error",
"errorSubtitle": "No se han podido recuperar los changesets", "errorSubtitle": "No se han podido recuperar los changesets",
"noChangesets": "No se han encontrado changesets para esta rama branch.", "noChangesets": "No se han encontrado changesets para esta rama branch.",
"branchSelectorLabel": "Ramas" "branchSelectorLabel": "Ramas",
"collapseDiffs": "Colapso"
}, },
"changeset": { "changeset": {
"description": "Descripción", "description": "Descripción",

View File

@@ -0,0 +1,42 @@
// @flow
import React from "react";
import PluginActionModal from "./PluginActionModal";
import type { PendingPlugins } from "@scm-manager/ui-types";
import { apiClient } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
type Props = {
onClose: () => void,
refresh: () => void,
pendingPlugins: PendingPlugins,
// context props
t: string => string
};
class CancelPendingActionModal extends React.Component<Props> {
render() {
const { onClose, pendingPlugins, t } = this.props;
return (
<PluginActionModal
description={t("plugins.modal.cancelPending")}
label={t("plugins.cancelPending")}
onClose={onClose}
pendingPlugins={pendingPlugins}
execute={this.cancelPending}
/>
);
}
cancelPending = () => {
const { pendingPlugins, refresh, onClose } = this.props;
return apiClient
.post(pendingPlugins._links.cancel.href)
.then(refresh)
.then(onClose);
};
}
export default translate("admin")(CancelPendingActionModal);

View File

@@ -1,68 +0,0 @@
// @flow
import React from "react";
import { Button } from "@scm-manager/ui-components";
import type { PendingPlugins } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import ExecutePendingModal from "./ExecutePendingModal";
type Props = {
pendingPlugins: PendingPlugins,
// context props
t: string => string
};
type State = {
showModal: boolean
};
class ExecutePendingAction extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showModal: false
};
}
openModal = () => {
this.setState({
showModal: true
});
};
closeModal = () => {
this.setState({
showModal: false
});
};
renderModal = () => {
const { showModal } = this.state;
const { pendingPlugins } = this.props;
if (showModal) {
return (
<ExecutePendingModal
pendingPlugins={pendingPlugins}
onClose={this.closeModal}
/>
);
}
return null;
};
render() {
const { t } = this.props;
return (
<>
{this.renderModal()}
<Button
color="primary"
label={t("plugins.executePending")}
action={this.openModal}
/>
</>
);
}
}
export default translate("admin")(ExecutePendingAction);

View File

@@ -0,0 +1,45 @@
// @flow
import React from "react";
import PluginActionModal from "./PluginActionModal";
import type { PendingPlugins } from "@scm-manager/ui-types";
import waitForRestart from "./waitForRestart";
import { apiClient, Notification } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
type Props = {
onClose: () => void,
pendingPlugins: PendingPlugins,
// context props
t: string => string
};
class ExecutePendingActionModal extends React.Component<Props> {
render() {
const { onClose, pendingPlugins, t } = this.props;
return (
<PluginActionModal
description={t("plugins.modal.executePending")}
label={t("plugins.modal.executeAndRestart")}
onClose={onClose}
pendingPlugins={pendingPlugins}
execute={this.executeAndRestart}
>
<Notification type="warning">
{t("plugins.modal.restartNotification")}
</Notification>
</PluginActionModal>
);
}
executeAndRestart = () => {
const { pendingPlugins } = this.props;
return apiClient
.post(pendingPlugins._links.execute.href)
.then(waitForRestart);
};
}
export default translate("admin")(ExecutePendingActionModal);

View File

@@ -1,21 +1,26 @@
// @flow // @flow
import React from "react"; import * as React from "react";
import { import {
apiClient,
Button, Button,
ButtonGroup, ButtonGroup,
ErrorNotification, ErrorNotification,
Modal, Modal
Notification
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import type { PendingPlugins } from "@scm-manager/ui-types"; import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import waitForRestart from "./waitForRestart";
import SuccessNotification from "./SuccessNotification"; import SuccessNotification from "./SuccessNotification";
type Props = { type Props = {
onClose: () => void, onClose: () => void,
pendingPlugins: PendingPlugins, actionType: string,
pendingPlugins?: PendingPlugins,
installedPlugins?: PluginCollection,
refresh: () => void,
execute: () => Promise<any>,
description: string,
label: string,
children?: React.Node,
// context props // context props
t: string => string t: string => string
@@ -27,7 +32,7 @@ type State = {
error?: Error error?: Error
}; };
class ExecutePendingModal extends React.Component<Props, State> { class PluginActionModal extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@@ -37,35 +42,28 @@ class ExecutePendingModal extends React.Component<Props, State> {
} }
renderNotifications = () => { renderNotifications = () => {
const { t } = this.props; const { children } = this.props;
const { error, success } = this.state; const { error, success } = this.state;
if (error) { if (error) {
return <ErrorNotification error={error} />; return <ErrorNotification error={error} />;
} else if (success) { } else if (success) {
return <SuccessNotification />; return <SuccessNotification />;
} else { } else {
return ( return children;
<Notification type="warning">
{t("plugins.modal.restartNotification")}
</Notification>
);
} }
}; };
executeAndRestart = () => { executeAction = () => {
const { pendingPlugins } = this.props;
this.setState({ this.setState({
loading: true loading: true
}); });
apiClient this.props
.post(pendingPlugins._links.execute.href) .execute()
.then(waitForRestart)
.then(() => { .then(() => {
this.setState({ this.setState({
success: true, success: true,
loading: false, loading: false
error: undefined
}); });
}) })
.catch(error => { .catch(error => {
@@ -77,11 +75,45 @@ class ExecutePendingModal extends React.Component<Props, State> {
}); });
}; };
renderModalContent = () => {
return (
<>
{this.renderUpdatable()}
{this.renderInstallQueue()}
{this.renderUpdateQueue()}
{this.renderUninstallQueue()}
</>
);
};
renderUpdatable = () => {
const { installedPlugins, t } = this.props;
return (
<>
{installedPlugins &&
installedPlugins._embedded &&
installedPlugins._embedded.plugins && (
<>
<strong>{t("plugins.modal.updateQueue")}</strong>
<ul>
{installedPlugins._embedded.plugins
.filter(plugin => plugin._links && plugin._links.update)
.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>
</>
)}
</>
);
};
renderInstallQueue = () => { renderInstallQueue = () => {
const { pendingPlugins, t } = this.props; const { pendingPlugins, t } = this.props;
return ( return (
<> <>
{pendingPlugins._embedded && {pendingPlugins &&
pendingPlugins._embedded &&
pendingPlugins._embedded.new.length > 0 && ( pendingPlugins._embedded.new.length > 0 && (
<> <>
<strong>{t("plugins.modal.installQueue")}</strong> <strong>{t("plugins.modal.installQueue")}</strong>
@@ -100,7 +132,8 @@ class ExecutePendingModal extends React.Component<Props, State> {
const { pendingPlugins, t } = this.props; const { pendingPlugins, t } = this.props;
return ( return (
<> <>
{pendingPlugins._embedded && {pendingPlugins &&
pendingPlugins._embedded &&
pendingPlugins._embedded.update.length > 0 && ( pendingPlugins._embedded.update.length > 0 && (
<> <>
<strong>{t("plugins.modal.updateQueue")}</strong> <strong>{t("plugins.modal.updateQueue")}</strong>
@@ -119,7 +152,8 @@ class ExecutePendingModal extends React.Component<Props, State> {
const { pendingPlugins, t } = this.props; const { pendingPlugins, t } = this.props;
return ( return (
<> <>
{pendingPlugins._embedded && {pendingPlugins &&
pendingPlugins._embedded &&
pendingPlugins._embedded.uninstall.length > 0 && ( pendingPlugins._embedded.uninstall.length > 0 && (
<> <>
<strong>{t("plugins.modal.uninstallQueue")}</strong> <strong>{t("plugins.modal.uninstallQueue")}</strong>
@@ -135,15 +169,12 @@ class ExecutePendingModal extends React.Component<Props, State> {
}; };
renderBody = () => { renderBody = () => {
const { t } = this.props;
return ( return (
<> <>
<div className="media"> <div className="media">
<div className="content"> <div className="content">
<p>{t("plugins.modal.executePending")}</p> <p>{this.props.description}</p>
{this.renderInstallQueue()} {this.renderModalContent()}
{this.renderUpdateQueue()}
{this.renderUninstallQueue()}
</div> </div>
</div> </div>
<div className="media">{this.renderNotifications()}</div> <div className="media">{this.renderNotifications()}</div>
@@ -158,9 +189,9 @@ class ExecutePendingModal extends React.Component<Props, State> {
<ButtonGroup> <ButtonGroup>
<Button <Button
color="warning" color="warning"
label={t("plugins.modal.executeAndRestart")} label={this.props.label}
loading={loading} loading={loading}
action={this.executeAndRestart} action={this.executeAction}
disabled={error || success} disabled={error || success}
/> />
<Button label={t("plugins.modal.abort")} action={onClose} /> <Button label={t("plugins.modal.abort")} action={onClose} />
@@ -169,10 +200,10 @@ class ExecutePendingModal extends React.Component<Props, State> {
}; };
render() { render() {
const { onClose, t } = this.props; const { onClose } = this.props;
return ( return (
<Modal <Modal
title={t("plugins.modal.executeAndRestart")} title={this.props.label}
closeFunction={onClose} closeFunction={onClose}
body={this.renderBody()} body={this.renderBody()}
footer={this.renderFooter()} footer={this.renderFooter()}
@@ -182,4 +213,4 @@ class ExecutePendingModal extends React.Component<Props, State> {
} }
} }
export default translate("admin")(ExecutePendingModal); export default translate("admin")(PluginActionModal);

View File

@@ -121,6 +121,7 @@ class PluginModal extends React.Component<Props, State> {
.catch(error => { .catch(error => {
this.setState({ this.setState({
loading: false, loading: false,
success: false,
error: error error: error
}); });
}); });

View File

@@ -0,0 +1,42 @@
// @flow
import React from "react";
import PluginActionModal from "./PluginActionModal";
import type { PluginCollection } from "@scm-manager/ui-types";
import { apiClient } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
type Props = {
onClose: () => void,
refresh: () => void,
installedPlugins: PluginCollection,
// context props
t: string => string
};
class UpdateAllActionModal extends React.Component<Props> {
render() {
const { onClose, installedPlugins, t } = this.props;
return (
<PluginActionModal
description={t("plugins.modal.updateAll")}
label={t("plugins.updateAll")}
onClose={onClose}
installedPlugins={installedPlugins}
execute={this.updateAll}
/>
);
}
updateAll = () => {
const { installedPlugins, refresh, onClose } = this.props;
return apiClient
.post(installedPlugins._links.update.href)
.then(refresh)
.then(onClose);
};
}
export default translate("admin")(UpdateAllActionModal);

View File

@@ -5,11 +5,13 @@ import { translate } from "react-i18next";
import { compose } from "redux"; import { compose } from "redux";
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types"; import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
import { import {
ButtonGroup,
ErrorNotification, ErrorNotification,
Loading, Loading,
Notification, Notification,
Subtitle, Subtitle,
Title Title,
Button
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import { import {
fetchPendingPlugins, fetchPendingPlugins,
@@ -27,7 +29,9 @@ import {
} from "../../../modules/indexResource"; } from "../../../modules/indexResource";
import PluginTopActions from "../components/PluginTopActions"; import PluginTopActions from "../components/PluginTopActions";
import PluginBottomActions from "../components/PluginBottomActions"; import PluginBottomActions from "../components/PluginBottomActions";
import ExecutePendingAction from "../components/ExecutePendingAction"; import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
import CancelPendingActionModal from "../components/CancelPendingActionModal";
import UpdateAllActionModal from "../components/UpdateAllActionModal";
type Props = { type Props = {
loading: boolean, loading: boolean,
@@ -41,14 +45,29 @@ type Props = {
pendingPlugins: PendingPlugins, pendingPlugins: PendingPlugins,
// context objects // context objects
t: string => string, t: (key: string, params?: Object) => string,
// dispatched functions // dispatched functions
fetchPluginsByLink: (link: string) => void, fetchPluginsByLink: (link: string) => void,
fetchPendingPlugins: (link: string) => void fetchPendingPlugins: (link: string) => void
}; };
class PluginsOverview extends React.Component<Props> { type State = {
showPendingModal: boolean,
showUpdateAllModal: boolean,
showCancelModal: boolean
};
class PluginsOverview extends React.Component<Props, State> {
constructor(props: Props, context: *) {
super(props, context);
this.state = {
showPendingModal: false,
showUpdateAllModal: false,
showCancelModal: false
};
}
componentDidMount() { componentDidMount() {
this.fetchPlugins(); this.fetchPlugins();
} }
@@ -102,17 +121,72 @@ class PluginsOverview extends React.Component<Props> {
}; };
createActions = () => { createActions = () => {
const { pendingPlugins } = this.props; const { pendingPlugins, collection, t } = this.props;
const buttons = [];
if ( if (
pendingPlugins && pendingPlugins &&
pendingPlugins._links && pendingPlugins._links &&
pendingPlugins._links.execute pendingPlugins._links.execute
) { ) {
return <ExecutePendingAction pendingPlugins={pendingPlugins} />; buttons.push(
<Button
color="primary"
reducedMobile={true}
key={"executePending"}
icon={"arrow-circle-right"}
label={t("plugins.executePending")}
action={() => this.setState({ showPendingModal: true })}
/>
);
}
if (
pendingPlugins &&
pendingPlugins._links &&
pendingPlugins._links.cancel
) {
buttons.push(
<Button
color="primary"
reducedMobile={true}
key={"cancelPending"}
icon={"times"}
label={t("plugins.cancelPending")}
action={() => this.setState({ showCancelModal: true })}
/>
);
}
if (collection && collection._links && collection._links.update) {
buttons.push(
<Button
color="primary"
reducedMobile={true}
key={"updateAll"}
icon={"sync-alt"}
label={this.computeUpdateAllSize()}
action={() => this.setState({ showUpdateAllModal: true })}
/>
);
}
if (buttons.length > 0) {
return <ButtonGroup>{buttons}</ButtonGroup>;
} }
return null; return null;
}; };
computeUpdateAllSize = () => {
const { collection, t } = this.props;
const outdatedPlugins = collection._embedded.plugins.filter(
p => p._links.update
).length;
return t("plugins.outdatedPlugins", {
count: outdatedPlugins
});
};
render() { render() {
const { loading, error, collection } = this.props; const { loading, error, collection } = this.props;
@@ -131,10 +205,47 @@ class PluginsOverview extends React.Component<Props> {
<hr className="header-with-actions" /> <hr className="header-with-actions" />
{this.renderPluginsList()} {this.renderPluginsList()}
{this.renderFooter(actions)} {this.renderFooter(actions)}
{this.renderModals()}
</> </>
); );
} }
renderModals = () => {
const { collection, pendingPlugins } = this.props;
const {
showPendingModal,
showCancelModal,
showUpdateAllModal
} = this.state;
if (showPendingModal) {
return (
<ExecutePendingActionModal
onClose={() => this.setState({ showPendingModal: false })}
pendingPlugins={pendingPlugins}
/>
);
}
if (showCancelModal) {
return (
<CancelPendingActionModal
onClose={() => this.setState({ showCancelModal: false })}
refresh={this.fetchPlugins}
pendingPlugins={pendingPlugins}
/>
);
}
if (showUpdateAllModal) {
return (
<UpdateAllActionModal
onClose={() => this.setState({ showUpdateAllModal: false })}
refresh={this.fetchPlugins}
installedPlugins={collection}
/>
);
}
};
renderPluginsList() { renderPluginsList() {
const { collection, t } = this.props; const { collection, t } = this.props;

View File

@@ -1,9 +1,10 @@
//@flow //@flow
import React from "react"; import React from "react";
import type { Changeset, Repository } from "@scm-manager/ui-types";
import { Interpolate, translate } from "react-i18next"; import { Interpolate, translate } from "react-i18next";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import classNames from "classnames";
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { import {
DateFromNow, DateFromNow,
ChangesetId, ChangesetId,
@@ -12,17 +13,31 @@ import {
ChangesetDiff, ChangesetDiff,
AvatarWrapper, AvatarWrapper,
AvatarImage, AvatarImage,
changesets changesets,
Level,
Button
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import classNames from "classnames"; type Props = {
import type { Tag } from "@scm-manager/ui-types"; changeset: Changeset,
import { ExtensionPoint } from "@scm-manager/ui-extensions"; repository: Repository,
// context props
t: string => string,
classes: any
};
type State = {
collapsed: boolean
};
const styles = { const styles = {
spacing: { spacing: {
marginRight: "1em" marginRight: "1em"
}, },
bottomMargin: {
marginBottom: "1rem !important"
},
tags: { tags: {
"& .tag": { "& .tag": {
marginLeft: ".25rem" marginLeft: ".25rem"
@@ -30,16 +45,17 @@ const styles = {
} }
}; };
type Props = { class ChangesetDetails extends React.Component<Props, State> {
changeset: Changeset, constructor(props: Props) {
repository: Repository, super(props);
t: string => string, this.state = {
classes: any collapsed: false
}; };
}
class ChangesetDetails extends React.Component<Props> {
render() { render() {
const { changeset, repository, classes } = this.props; const { changeset, repository, classes, t } = this.props;
const { collapsed } = this.state;
const description = changesets.parseDescription(changeset.description); const description = changesets.parseDescription(changeset.description);
@@ -49,8 +65,8 @@ class ChangesetDetails extends React.Component<Props> {
const date = <DateFromNow date={changeset.date} />; const date = <DateFromNow date={changeset.date} />;
return ( return (
<div> <>
<div className="content"> <div className="content is-marginless">
<h4> <h4>
<ExtensionPoint <ExtensionPoint
name="changeset.description" name="changeset.description"
@@ -71,16 +87,11 @@ class ChangesetDetails extends React.Component<Props> {
<ChangesetAuthor changeset={changeset} /> <ChangesetAuthor changeset={changeset} />
</p> </p>
<p> <p>
<Interpolate <Interpolate i18nKey="changeset.summary" id={id} time={date} />
i18nKey="changeset.summary"
id={id}
time={date}
/>
</p> </p>
</div> </div>
<div className="media-right">{this.renderTags()}</div> <div className="media-right">{this.renderTags()}</div>
</article> </article>
<p> <p>
{description.message.split("\n").map((item, key) => { {description.message.split("\n").map((item, key) => {
return ( return (
@@ -99,9 +110,21 @@ class ChangesetDetails extends React.Component<Props> {
</p> </p>
</div> </div>
<div> <div>
<ChangesetDiff changeset={changeset} /> <Level
</div> className={classes.bottomMargin}
right={
<Button
action={this.collapseDiffs}
color="default"
icon={collapsed ? "eye" : "eye-slash"}
label={t("changesets.collapseDiffs")}
reducedMobile={true}
/>
}
/>
<ChangesetDiff changeset={changeset} defaultCollapse={collapsed} />
</div> </div>
</>
); );
} }
@@ -124,6 +147,12 @@ class ChangesetDetails extends React.Component<Props> {
} }
return null; return null;
}; };
collapseDiffs = () => {
this.setState(state => ({
collapsed: !state.collapsed
}));
};
} }
export default injectSheet(styles)(translate("repos")(ChangesetDetails)); export default injectSheet(styles)(translate("repos")(ChangesetDetails));

View File

@@ -71,5 +71,5 @@ export default withRouter(
connect( connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(translate("changesets")(ChangesetView)) )(translate("repos")(ChangesetView))
); );

View File

@@ -4,6 +4,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
@@ -20,6 +21,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput; import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
public class DiffRootResource { public class DiffRootResource {
@@ -55,20 +57,17 @@ public class DiffRootResource {
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"), @ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ){ public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ) throws IOException {
HttpUtil.checkForCRLFInjection(revision); HttpUtil.checkForCRLFInjection(revision);
DiffFormat diffFormat = DiffFormat.valueOf(format); DiffFormat diffFormat = DiffFormat.valueOf(format);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
StreamingOutput responseEntry = output -> { DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand()
repositoryService.getDiffCommand()
.setRevision(revision) .setRevision(revision)
.setFormat(diffFormat) .setFormat(diffFormat)
.retrieveContent(output); .retrieveContent();
}; return Response.ok((StreamingOutput) outputStreamConsumer::accept)
return Response.ok(responseEntry)
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision))) .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision)))
.build(); .build();
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.DiffFormat;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
@@ -138,14 +139,13 @@ public class IncomingRootResource {
HttpUtil.checkForCRLFInjection(target); HttpUtil.checkForCRLFInjection(target);
DiffFormat diffFormat = DiffFormat.valueOf(format); DiffFormat diffFormat = DiffFormat.valueOf(format);
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
StreamingOutput responseEntry = output -> DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand()
repositoryService.getDiffCommand()
.setRevision(source) .setRevision(source)
.setAncestorChangeset(target) .setAncestorChangeset(target)
.setFormat(diffFormat) .setFormat(diffFormat)
.retrieveContent(output); .retrieveContent();
return Response.ok(responseEntry) return Response.ok((StreamingOutput) outputStreamConsumer::accept)
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, source))) .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, source)))
.build(); .build();
} }

View File

@@ -56,6 +56,21 @@ public class InstalledPluginResource {
return Response.ok(collectionMapper.mapInstalled(plugins, available)).build(); return Response.ok(collectionMapper.mapInstalled(plugins, available)).build();
} }
/**
* Updates all installed plugins.
*/
@POST
@Path("/update")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(CollectionDto.class)
public Response updateAll() {
pluginManager.updateAll();
return Response.ok().build();
}
/** /**
* Returns the installed plugin with the given id. * Returns the installed plugin with the given id.
* *

View File

@@ -46,8 +46,6 @@ public class PendingPluginResource {
}) })
@Produces(VndMediaType.PLUGIN_COLLECTION) @Produces(VndMediaType.PLUGIN_COLLECTION)
public Response getPending() { public Response getPending() {
PluginPermissions.manage().check();
List<AvailablePlugin> pending = pluginManager List<AvailablePlugin> pending = pluginManager
.getAvailable() .getAvailable()
.stream() .stream()
@@ -71,8 +69,12 @@ public class PendingPluginResource {
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) { if (
PluginPermissions.manage().isPermitted() &&
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
) {
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending())); linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
} }
Embedded.Builder embedded = Embedded.embeddedBuilder(); Embedded.Builder embedded = Embedded.embeddedBuilder();
@@ -106,8 +108,18 @@ public class PendingPluginResource {
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response executePending() { public Response executePending() {
PluginPermissions.manage().check();
pluginManager.executePendingAndRestart(); pluginManager.executePendingAndRestart();
return Response.ok().build(); return Response.ok().build();
} }
@POST
@Path("/cancel")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response cancelPending() {
pluginManager.cancelPending();
return Response.ok().build();
}
} }

View File

@@ -3,15 +3,15 @@ package sonia.scm.api.v2.resources;
import com.google.inject.Inject; import com.google.inject.Inject;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginManager;
import java.util.List; import java.util.List;
import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo; import static de.otto.edison.hal.Links.linkingTo;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
@@ -19,11 +19,13 @@ public class PluginDtoCollectionMapper {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final PluginDtoMapper mapper; private final PluginDtoMapper mapper;
private final PluginManager manager;
@Inject @Inject
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper) { public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper, PluginManager manager) {
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.mapper = mapper; this.mapper = mapper;
this.manager = manager;
} }
public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) { public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) {
@@ -44,6 +46,11 @@ public class PluginDtoCollectionMapper {
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(baseUrl).build()); .with(Links.linkingTo().self(baseUrl).build());
if (!manager.getUpdatable().isEmpty()) {
linksBuilder.single(link("update", resourceLinks.installedPluginCollection().update()));
}
return linksBuilder.build(); return linksBuilder.build();
} }

View File

@@ -686,6 +686,10 @@ class ResourceLinks {
String self() { String self() {
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href(); return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href();
} }
String update() {
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("updateAll").parameters().href();
}
} }
public AvailablePluginLinks availablePlugin() { public AvailablePluginLinks availablePlugin() {
@@ -739,6 +743,10 @@ class ResourceLinks {
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href(); return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href();
} }
String cancelPending() {
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("cancelPending").parameters().href();
}
String self() { String self() {
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href(); return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href();
} }

View File

@@ -44,8 +44,8 @@ import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.version.Version; import sonia.scm.version.Version;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -72,7 +72,8 @@ public class DefaultPluginManager implements PluginManager {
private final PluginLoader loader; private final PluginLoader loader;
private final PluginCenter center; private final PluginCenter center;
private final PluginInstaller installer; private final PluginInstaller installer;
private final Collection<PendingPluginInstallation> pendingQueue = new ArrayList<>(); private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@Inject @Inject
@@ -106,7 +107,7 @@ public class DefaultPluginManager implements PluginManager {
} }
private Optional<AvailablePlugin> getPending(String name) { private Optional<AvailablePlugin> getPending(String name) {
return pendingQueue return pendingInstallQueue
.stream() .stream()
.map(PendingPluginInstallation::getPlugin) .map(PendingPluginInstallation::getPlugin)
.filter(filterByName(name)) .filter(filterByName(name))
@@ -138,6 +139,15 @@ public class DefaultPluginManager implements PluginManager {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public List<InstalledPlugin> getUpdatable() {
return getInstalled()
.stream()
.filter(p -> isUpdatable(p.getDescriptor().getInformation().getName()))
.filter(p -> !p.isMarkedForUninstall())
.collect(Collectors.toList());
}
private <T extends Plugin> Predicate<T> filterByName(String name) { private <T extends Plugin> Predicate<T> filterByName(String name) {
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName()); return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
} }
@@ -179,7 +189,7 @@ public class DefaultPluginManager implements PluginManager {
if (restartAfterInstallation) { if (restartAfterInstallation) {
restart("plugin installation"); restart("plugin installation");
} else { } else {
pendingQueue.addAll(pendingInstallations); pendingInstallQueue.addAll(pendingInstallations);
updateMayUninstallFlag(); updateMayUninstallFlag();
} }
} }
@@ -192,10 +202,7 @@ public class DefaultPluginManager implements PluginManager {
.orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name)));
doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore()); doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore());
dependencyTracker.removeInstalled(installed.getDescriptor()); markForUninstall(installed);
installed.setMarkedForUninstall(true);
createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME);
if (restartAfterInstallation) { if (restartAfterInstallation) {
restart("plugin installation"); restart("plugin installation");
@@ -215,18 +222,22 @@ public class DefaultPluginManager implements PluginManager {
&& dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName()); && dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName());
} }
private void createMarkerFile(InstalledPlugin plugin, String markerFile) { private void markForUninstall(InstalledPlugin plugin) {
dependencyTracker.removeInstalled(plugin.getDescriptor());
try { try {
Files.createFile(plugin.getDirectory().resolve(markerFile)); Path file = Files.createFile(plugin.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME));
} catch (IOException e) { pendingUninstallQueue.add(new PendingPluginUninstallation(plugin, file));
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + markerFile, e); plugin.setMarkedForUninstall(true);
} catch (Exception e) {
dependencyTracker.addInstalled(plugin.getDescriptor());
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + InstalledPlugin.UNINSTALL_MARKER_FILENAME, e);
} }
} }
@Override @Override
public void executePendingAndRestart() { public void executePendingAndRestart() {
PluginPermissions.manage().check(); PluginPermissions.manage().check();
if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) { if (!pendingInstallQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
restart("execute pending plugin changes"); restart("execute pending plugin changes");
} }
} }
@@ -269,4 +280,25 @@ public class DefaultPluginManager implements PluginManager {
private boolean isUpdatable(String name) { private boolean isUpdatable(String name) {
return getAvailable(name).isPresent() && !getPending(name).isPresent(); return getAvailable(name).isPresent() && !getPending(name).isPresent();
} }
@Override
public void cancelPending() {
PluginPermissions.manage().check();
pendingUninstallQueue.forEach(PendingPluginUninstallation::cancel);
pendingInstallQueue.forEach(PendingPluginInstallation::cancel);
pendingUninstallQueue.clear();
pendingInstallQueue.clear();
updateMayUninstallFlag();
}
@Override
public void updateAll() {
PluginPermissions.manage().check();
for (InstalledPlugin installedPlugin : getInstalled()) {
String pluginName = installedPlugin.getDescriptor().getInformation().getName();
if (isUpdatable(pluginName)) {
install(pluginName, false);
}
}
}
} }

View File

@@ -29,7 +29,7 @@ class PendingPluginInstallation {
try { try {
Files.delete(file); Files.delete(file);
} catch (IOException ex) { } catch (IOException ex) {
throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex); throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex);
} }
} }
} }

View File

@@ -0,0 +1,32 @@
package sonia.scm.plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class PendingPluginUninstallation {
private static final Logger LOG = LoggerFactory.getLogger(PendingPluginUninstallation.class);
private final InstalledPlugin plugin;
private final Path uninstallFile;
PendingPluginUninstallation(InstalledPlugin plugin, Path uninstallFile) {
this.plugin = plugin;
this.uninstallFile = uninstallFile;
}
void cancel() {
String name = plugin.getDescriptor().getInformation().getName();
LOG.info("cancel uninstallation of plugin {}", name);
try {
Files.delete(uninstallFile);
plugin.setMarkedForUninstall(false);
} catch (IOException ex) {
throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation", name, ex);
}
}
}

View File

@@ -33,6 +33,10 @@ class PluginDependencyTracker {
} }
private void removeDependency(String from, String to) { private void removeDependency(String from, String to) {
plugins.get(to).remove(from); Collection<String> dependencies = plugins.get(to);
if (dependencies == null) {
throw new NullPointerException("inverse dependencies not found for " + to);
}
dependencies.remove(from);
} }
} }

View File

@@ -1,7 +1,16 @@
package sonia.scm.plugin; package sonia.scm.plugin;
public class PluginFailedToCancelInstallationException extends RuntimeException { import sonia.scm.ExceptionWithContext;
public PluginFailedToCancelInstallationException(String message, Throwable cause) {
super(message, cause); import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class PluginFailedToCancelInstallationException extends ExceptionWithContext {
public PluginFailedToCancelInstallationException(String message, String name, Exception cause) {
super(entity("plugin", name).build(), message, cause);
}
@Override
public String getCode() {
return "65RdZ5atX1";
} }
} }

View File

@@ -175,6 +175,14 @@
"40RaYIeeR1": { "40RaYIeeR1": {
"displayName": "Es wurden keine Änderungen durchgeführt", "displayName": "Es wurden keine Änderungen durchgeführt",
"description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden." "description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden."
},
"4iRct4avG1": {
"displayName": "Die Revisionen haben keinen gemeinsamen Ursprung",
"description": "Die Historie der Revisionen hat keinen gemeinsamen Urspung und kann somit auch nicht gegen einen solchen verglichen werden."
},
"65RdZ5atX1": {
"displayName": "Fehler beim Löschen von Plugin-Dateien",
"description": "Einige Dateien für die Plugin-Deinstallation konnten nicht gelöscht werden. Dieses kann zu Inkonsistenzen führen, so dass der SCM-Manager nicht mehr korrekt starten kann. Bitte prüfen Sie die Logs und bereinigen Sie das Plugin-Verzeichnis des SCM-Managers manuell. Um die Installation eines Plugins abzubrechen, löschen Sie die zugehörige smp Datei aus dem Plugin-Verzeichnis. Um ein Entfernen eines Plugins zu verhindern, entfernen Sie die Datei namens 'uninstall' aus dem entsprechenden Verzeichnis des Plugins."
} }
}, },
"namespaceStrategies": { "namespaceStrategies": {

View File

@@ -175,6 +175,14 @@
"40RaYIeeR1": { "40RaYIeeR1": {
"displayName": "No changes were made", "displayName": "No changes were made",
"description": "No changes were made to the files of the repository. Therefor no new commit could be created." "description": "No changes were made to the files of the repository. Therefor no new commit could be created."
},
"4iRct4avG1": {
"displayName": "The revisions have unrelated histories",
"description": "The revisions have unrelated histories. Therefor there is no common commit to compare with."
},
"65RdZ5atX1": {
"displayName": "Error removing plugin files",
"description": "Some files to cancel the plugin (un)installation could not be deleted. This can lead to inconsistencies so that the SCM-Manager cannot restart properly. Please check the logs and clean up the plugin folder manually. To cancel the installation of a plugin, remove the corresponding smp file. To cancel the uninstallation, remove the file named 'uninstall' inside the directory for this plugin."
} }
}, },
"namespaceStrategies": { "namespaceStrategies": {

View File

@@ -91,7 +91,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGetDiffs() throws Exception { public void shouldGetDiffs() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision") .get(DIFF_URL + "revision")
.accept(VndMediaType.DIFF); .accept(VndMediaType.DIFF);
@@ -123,7 +123,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGet404OnMissingRevision() throws Exception { public void shouldGet404OnMissingRevision() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision") .get(DIFF_URL + "revision")
@@ -139,7 +139,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGet400OnCrlfInjection() throws Exception { public void shouldGet400OnCrlfInjection() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634") .get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634")
@@ -153,7 +153,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldGet400OnUnknownFormat() throws Exception { public void shouldGet400OnUnknownFormat() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Test", "test")); when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(DIFF_URL + "revision?format=Unknown") .get(DIFF_URL + "revision?format=Unknown")
@@ -167,7 +167,7 @@ public class DiffResourceTest extends RepositoryTestBase {
public void shouldAcceptDiffFormats() throws Exception { public void shouldAcceptDiffFormats() throws Exception {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach( Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach(
this::assertRequestOk this::assertRequestOk

View File

@@ -171,7 +171,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff") .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
.accept(VndMediaType.DIFF); .accept(VndMediaType.DIFF);
@@ -206,7 +206,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff") .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
@@ -223,7 +223,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff") .get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff")
.accept(VndMediaType.DIFF); .accept(VndMediaType.DIFF);
@@ -240,7 +240,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Test", "test")); when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown") .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown")
.accept(VndMediaType.DIFF); .accept(VndMediaType.DIFF);

View File

@@ -34,11 +34,8 @@ import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -90,7 +87,7 @@ class PendingPluginResourceTest {
@BeforeEach @BeforeEach
void bindSubject() { void bindSubject() {
ThreadContext.bind(subject); ThreadContext.bind(subject);
doNothing().when(subject).checkPermission("plugin:manage"); lenient().when(subject.isPermitted("plugin:manage")).thenReturn(true);
} }
@AfterEach @AfterEach
@@ -113,7 +110,7 @@ class PendingPluginResourceTest {
} }
@Test @Test
void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { void shouldGetPendingAvailablePluginListWithInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException {
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
when(availablePlugin.isPending()).thenReturn(true); when(availablePlugin.isPending()).thenReturn(true);
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
@@ -124,6 +121,7 @@ class PendingPluginResourceTest {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
assertThat(response.getContentAsString()).contains("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}");
} }
@Test @Test
@@ -166,6 +164,17 @@ class PendingPluginResourceTest {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
verify(pluginManager).executePendingAndRestart(); verify(pluginManager).executePendingAndRestart();
} }
@Test
void shouldCancelPendingPlugins() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/cancel");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
verify(pluginManager).cancelPending();
}
} }
@Nested @Nested
@@ -174,7 +183,7 @@ class PendingPluginResourceTest {
@BeforeEach @BeforeEach
void bindSubject() { void bindSubject() {
ThreadContext.bind(subject); ThreadContext.bind(subject);
doThrow(new ShiroException()).when(subject).checkPermission("plugin:manage"); when(subject.isPermitted("plugin:manage")).thenReturn(false);
} }
@AfterEach @AfterEach
@@ -183,23 +192,18 @@ class PendingPluginResourceTest {
} }
@Test @Test
void shouldNotListPendingPlugins() throws URISyntaxException { void shouldGetPendingAvailablePluginListWithoutInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException {
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
when(availablePlugin.isPending()).thenReturn(true);
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending");
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
verify(pluginManager, never()).executePendingAndRestart(); assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
} assertThat(response.getContentAsString()).doesNotContain("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
assertThat(response.getContentAsString()).doesNotContain("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}");
@Test
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()).executePendingAndRestart();
} }
} }

View File

@@ -10,15 +10,19 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginManager;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -34,6 +38,9 @@ class PluginDtoCollectionMapperTest {
@InjectMocks @InjectMocks
PluginDtoMapperImpl pluginDtoMapper; PluginDtoMapperImpl pluginDtoMapper;
@Mock
PluginManager manager;
Subject subject = mock(Subject.class); Subject subject = mock(Subject.class);
ThreadState subjectThreadState = new SubjectThreadState(subject); ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -43,6 +50,11 @@ class PluginDtoCollectionMapperTest {
ThreadContext.bind(subject); ThreadContext.bind(subject);
} }
@BeforeEach
void mockPluginManager() {
lenient().when(manager.getUpdatable()).thenReturn(new ArrayList<>());
}
@AfterEach @AfterEach
public void unbindSubject() { public void unbindSubject() {
ThreadContext.unbindSubject(); ThreadContext.unbindSubject();
@@ -51,7 +63,7 @@ class PluginDtoCollectionMapperTest {
@Test @Test
void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() { void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() {
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
HalRepresentation result = mapper.mapInstalled( HalRepresentation result = mapper.mapInstalled(
singletonList(createInstalledPlugin("scm-some-plugin", "1")), singletonList(createInstalledPlugin("scm-some-plugin", "1")),
@@ -66,7 +78,7 @@ class PluginDtoCollectionMapperTest {
@Test @Test
void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() { void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() {
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper,manager);
HalRepresentation result = mapper.mapInstalled( HalRepresentation result = mapper.mapInstalled(
singletonList(createInstalledPlugin("scm-some-plugin", "1")), singletonList(createInstalledPlugin("scm-some-plugin", "1")),
@@ -80,7 +92,7 @@ class PluginDtoCollectionMapperTest {
@Test @Test
void shouldNotAddInstallLinkForNewVersionWhenNotPermitted() { void shouldNotAddInstallLinkForNewVersionWhenNotPermitted() {
when(subject.isPermitted("plugin:manage")).thenReturn(false); when(subject.isPermitted("plugin:manage")).thenReturn(false);
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
HalRepresentation result = mapper.mapInstalled( HalRepresentation result = mapper.mapInstalled(
singletonList(createInstalledPlugin("scm-some-plugin", "1")), singletonList(createInstalledPlugin("scm-some-plugin", "1")),
@@ -93,7 +105,7 @@ class PluginDtoCollectionMapperTest {
@Test @Test
void shouldNotAddInstallLinkForNewVersionWhenInstallationIsPending() { void shouldNotAddInstallLinkForNewVersionWhenInstallationIsPending() {
when(subject.isPermitted("plugin:manage")).thenReturn(true); when(subject.isPermitted("plugin:manage")).thenReturn(true);
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
when(availablePlugin.isPending()).thenReturn(true); when(availablePlugin.isPending()).thenReturn(true);
@@ -108,7 +120,7 @@ class PluginDtoCollectionMapperTest {
@Test @Test
void shouldAddInstallLinkForNewVersionWhenPermitted() { void shouldAddInstallLinkForNewVersionWhenPermitted() {
when(subject.isPermitted("plugin:manage")).thenReturn(true); when(subject.isPermitted("plugin:manage")).thenReturn(true);
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
HalRepresentation result = mapper.mapInstalled( HalRepresentation result = mapper.mapInstalled(
singletonList(createInstalledPlugin("scm-some-plugin", "1")), singletonList(createInstalledPlugin("scm-some-plugin", "1")),
@@ -121,7 +133,7 @@ class PluginDtoCollectionMapperTest {
@Test @Test
void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() { void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() {
when(subject.isPermitted("plugin:manage")).thenReturn(true); when(subject.isPermitted("plugin:manage")).thenReturn(true);
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
when(availablePlugin.isPending()).thenReturn(true); when(availablePlugin.isPending()).thenReturn(true);

View File

@@ -20,6 +20,8 @@ import sonia.scm.ScmConstraintViolationException;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.RestartEvent; import sonia.scm.lifecycle.RestartEvent;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -30,11 +32,13 @@ import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createAvailable;
@@ -358,6 +362,24 @@ class DefaultPluginManagerTest {
verify(mailPlugin).setMarkedForUninstall(true); verify(mailPlugin).setMarkedForUninstall(true);
} }
@Test
void shouldNotChangeStateWhenUninstallFileCouldNotBeCreated() {
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin");
when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin"));
when(reviewPlugin.getDirectory()).thenThrow(new PluginException("when the file could not be written an exception like this is thrown"));
when(loader.getInstalledPlugins()).thenReturn(asList(mailPlugin, reviewPlugin));
manager.computeInstallationDependencies();
assertThrows(PluginException.class, () -> manager.uninstall("scm-review-plugin", false));
verify(mailPlugin, never()).setMarkedForUninstall(true);
assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false));
}
@Test @Test
void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) { void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) {
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
@@ -427,6 +449,71 @@ class DefaultPluginManagerTest {
verify(eventBus).post(any(RestartEvent.class)); verify(eventBus).post(any(RestartEvent.class));
} }
@Test
void shouldUndoPendingInstallations(@TempDirectory.TempDir Path temp) throws IOException {
InstalledPlugin mailPlugin = createInstalled("scm-ssh-plugin");
Path mailPluginPath = temp.resolve("scm-mail-plugin");
Files.createDirectories(mailPluginPath);
when(mailPlugin.getDirectory()).thenReturn(mailPluginPath);
when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin));
ArgumentCaptor<Boolean> uninstallCaptor = ArgumentCaptor.forClass(Boolean.class);
doNothing().when(mailPlugin).setMarkedForUninstall(uninstallCaptor.capture());
AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class);
when(installer.install(git)).thenReturn(gitPendingPluginInformation);
manager.install("scm-git-plugin", false);
manager.uninstall("scm-ssh-plugin", false);
manager.cancelPending();
assertThat(mailPluginPath.resolve("uninstall")).doesNotExist();
verify(gitPendingPluginInformation).cancel();
Boolean lasUninstallMarkerSet = uninstallCaptor.getAllValues().get(uninstallCaptor.getAllValues().size() - 1);
assertThat(lasUninstallMarkerSet).isFalse();
Files.createFile(mailPluginPath.resolve("uninstall"));
manager.cancelPending();
verify(gitPendingPluginInformation, times(1)).cancel();
assertThat(mailPluginPath.resolve("uninstall")).exists();
}
@Test
void shouldUpdateAllPlugins() {
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin));
AvailablePlugin newMailPlugin = createAvailable("scm-mail-plugin", "2.0.0");
AvailablePlugin newReviewPlugin = createAvailable("scm-review-plugin", "2.0.0");
when(center.getAvailable()).thenReturn(ImmutableSet.of(newMailPlugin, newReviewPlugin));
manager.updateAll();
verify(installer).install(newMailPlugin);
verify(installer).install(newReviewPlugin);
}
@Test
void shouldNotUpdateToOldPluginVersions() {
InstalledPlugin scriptPlugin = createInstalled("scm-script-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(scriptPlugin));
AvailablePlugin oldScriptPlugin = createAvailable("scm-script-plugin", "0.9");
when(center.getAvailable()).thenReturn(ImmutableSet.of(oldScriptPlugin));
manager.updateAll();
verify(installer, never()).install(oldScriptPlugin);
}
} }
@Nested @Nested
@@ -482,5 +569,14 @@ class DefaultPluginManagerTest {
assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart()); assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart());
} }
@Test
void shouldThrowAuthorizationExceptionsForCancelPending() {
assertThrows(AuthorizationException.class, () -> manager.cancelPending());
}
@Test
void shouldThrowAuthorizationExceptionsForUpdateAll() {
assertThrows(AuthorizationException.class, () -> manager.updateAll());
}
} }
} }

View File

@@ -14,6 +14,13 @@ public class PluginTestHelper {
return createAvailable(information); return createAvailable(information);
} }
public static AvailablePlugin createAvailable(String name, String version) {
PluginInformation information = new PluginInformation();
information.setName(name);
information.setVersion(version);
return createAvailable(information);
}
public static InstalledPlugin createInstalled(String name) { public static InstalledPlugin createInstalled(String name) {
PluginInformation information = new PluginInformation(); PluginInformation information = new PluginInformation();
information.setName(name); information.setName(name);