From e533820ae6f939cf27a5096a46fd0c7a546fafbe Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 23 Sep 2019 12:16:07 +0200 Subject: [PATCH 01/80] Check for unrelated histories --- .../java/sonia/scm/repository/GitUtil.java | 6 +++-- .../java/sonia/scm/repository/spi/Differ.java | 22 +++++++++++++++++-- .../main/resources/locales/de/plugins.json | 4 ++++ .../main/resources/locales/en/plugins.json | 4 ++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 7175d3b646..992e595382 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -70,6 +70,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import static java.util.Optional.of; +import static java.util.Optional.ofNullable; //~--- JDK imports ------------------------------------------------------------ @@ -722,12 +723,13 @@ public final class GitUtil /** * Computes the first common ancestor of two revisions, aka merge base. */ - public static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + public static Optional computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { try (RevWalk mergeBaseWalk = new RevWalk(repository)) { mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE); mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1)); mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2)); - return mergeBaseWalk.next().getId(); + RevCommit commonAncestor = mergeBaseWalk.next(); + return ofNullable(commonAncestor).map(RevCommit::getId); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java index ca417550f4..81d19886c3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/Differ.java @@ -10,11 +10,15 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; +import sonia.scm.BadRequestException; import sonia.scm.repository.GitUtil; import sonia.scm.util.Util; import java.io.IOException; import java.util.List; +import java.util.Optional; + +import static java.util.Collections.emptyList; final class Differ implements AutoCloseable { @@ -55,7 +59,9 @@ final class Differ implements AutoCloseable { if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision); + ObjectId ancestorId = + computeCommonAncestor(repository, revision, otherRevision) + .orElseThrow(NoCommonHistoryException::new); RevTree tree = walk.parseCommit(ancestorId).getTree(); treeWalk.addTree(tree); } @@ -82,7 +88,7 @@ final class Differ implements AutoCloseable { return new Differ(commit, walk, treeWalk); } - private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + private static Optional computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { return GitUtil.computeCommonAncestor(repository, revision1, revision2); } @@ -115,4 +121,16 @@ final class Differ implements AutoCloseable { return entries; } } + + private static class NoCommonHistoryException extends BadRequestException { + + private NoCommonHistoryException() { + super(emptyList(), "no common history"); + } + + @Override + public String getCode() { + return "4iRct4avG1"; + } + } } diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 9c5bb3f5f5..ef6d5d8c96 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -175,6 +175,10 @@ "40RaYIeeR1": { "displayName": "Es wurden keine Änderungen durchgeführt", "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." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 1ffffd73b7..247fa212f3 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -175,6 +175,10 @@ "40RaYIeeR1": { "displayName": "No changes were made", "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." } }, "namespaceStrategies": { From 7e5e45b488bc32ec31740c3ed4d829a089b855ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 24 Sep 2019 20:50:54 +0200 Subject: [PATCH 02/80] Detect and load lfs files --- .../scm/repository/spi/GitBrowseCommand.java | 11 ++- .../scm/repository/spi/GitCatCommand.java | 92 +++++++++++++++--- .../spi/GitRepositoryServiceProvider.java | 16 +-- .../spi/GitRepositoryServiceResolver.java | 7 +- .../repository/spi/GitBrowseCommandTest.java | 2 +- .../scm/repository/spi/GitCatCommandTest.java | 35 ++++++- .../scm/repository/spi/scm-git-spi-test.zip | Bin 23641 -> 26072 bytes 7 files changed, 135 insertions(+), 28 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 2a254d96ce..1bc59f8e1b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -58,6 +58,7 @@ import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.SubRepository; import sonia.scm.util.Util; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -86,18 +87,20 @@ public class GitBrowseCommand extends AbstractGitCommand */ private static final Logger logger = LoggerFactory.getLogger(GitBrowseCommand.class); + private final LfsBlobStoreFactory lfsBlobStoreFactory; //~--- constructors --------------------------------------------------------- /** * Constructs ... - * - * @param context + * @param context * @param repository + * @param lfsBlobStoreFactory */ - public GitBrowseCommand(GitContext context, Repository repository) + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context, repository); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } //~--- get methods ---------------------------------------------------------- @@ -375,7 +378,7 @@ public class GitBrowseCommand extends AbstractGitCommand Map subRepositories; try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { - new GitCatCommand(context, repository).getContent(repo, revision, + new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision, PATH_MODULES, baos); subRepositories = GitSubModuleParser.parse(baos.toString()); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 7477e0aee3..21f4b942df 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -32,7 +32,10 @@ package sonia.scm.repository.spi; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -42,10 +45,14 @@ import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.util.LfsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; +import sonia.scm.store.Blob; +import sonia.scm.util.IOUtil; import sonia.scm.util.Util; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.Closeable; import java.io.FilterInputStream; @@ -61,15 +68,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { 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); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } @Override public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException { logger.debug("try to read content for {}", request); - try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) { - closableObjectLoaderContainer.objectLoader.copyTo(output); + try (Loader closableObjectLoaderContainer = getLoader(request)) { + closableObjectLoaderContainer.copyTo(output); } } @@ -80,18 +90,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { } void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException { - try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) { - closableObjectLoaderContainer.objectLoader.copyTo(output); + try (Loader closableObjectLoaderContainer = getLoader(repo, revId, path)) { + closableObjectLoaderContainer.copyTo(output); } } - private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException { + private Loader getLoader(CatCommandRequest request) throws IOException { org.eclipse.jgit.lib.Repository repo = open(); ObjectId revId = getCommitOrDefault(repo, request.getRevision()); 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.setRecursive(Util.nonNull(path).contains("/")); @@ -116,21 +126,69 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { treeWalk.setFilter(PathFilter.create(path)); if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { + Attributes attributes = LfsFactory.getAttributesForPath(repo, path, entry); + + Attribute filter = attributes.get("filter"); + if (filter != null && "lfs".equals(filter.getValue())) { + return loadFromLfsStore(repo, treeWalk, revWalk); + } + ObjectId blobId = treeWalk.getObjectId(0); ObjectLoader loader = repo.open(blobId); - return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk); + return new GitObjectLoaderWrapper(loader, treeWalk, revWalk); } else { throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository)); } } - private static class ClosableObjectLoaderContainer implements Closeable { + private Loader loadFromLfsStore(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + LfsPointer lfsPointer; + try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) { + lfsPointer = LfsPointer.parseLfsPointer(is); + } + Blob blob = lfsBlobStoreFactory.getLfsBlobStore(repository).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 TreeWalk treeWalk; private final RevWalk revWalk; - private ClosableObjectLoaderContainer(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { + private GitObjectLoaderWrapper(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { this.objectLoader = objectLoader; this.treeWalk = treeWalk; this.revWalk = revWalk; @@ -141,14 +199,22 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { GitUtil.release(revWalk); 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 final ClosableObjectLoaderContainer container; + private final Loader container; - private InputStreamWrapper(ClosableObjectLoaderContainer container) throws IOException { - super(container.objectLoader.openStream()); + private InputStreamWrapper(Loader container) throws IOException { + super(container.openStream()); this.container = container; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 24dcff01d8..4c02a3a73c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -39,6 +39,7 @@ import sonia.scm.repository.Feature; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.IOException; import java.util.EnumSet; @@ -76,9 +77,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider) { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { this.handler = handler; this.repository = repository; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } @@ -143,7 +145,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BrowseCommand getBrowseCommand() { - return new GitBrowseCommand(context, repository); + return new GitBrowseCommand(context, repository, lfsBlobStoreFactory); } /** @@ -155,7 +157,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public CatCommand getCatCommand() { - return new GitCatCommand(context, repository); + return new GitCatCommand(context, repository, lfsBlobStoreFactory); } /** @@ -281,11 +283,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- fields --------------------------------------------------------------- /** Field description */ - private GitContext context; + private final GitContext context; /** Field description */ - private GitRepositoryHandler handler; + private final GitRepositoryHandler handler; /** Field description */ - private Repository repository; + private final Repository repository; + + private final LfsBlobStoreFactory lfsBlobStoreFactory; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 0730ffc9cf..547c6b25f8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -39,6 +39,7 @@ import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; 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 GitRepositoryConfigStoreProvider storeProvider; + private final LfsBlobStoreFactory lfsBlobStoreFactory; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { this.handler = handler; this.storeProvider = storeProvider; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } @Override @@ -61,7 +64,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { GitRepositoryServiceProvider provider = null; if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider); + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory); } return provider; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 1feceba652..4b854f6209 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -171,6 +171,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } private GitBrowseCommand createCommand() { - return new GitBrowseCommand(createContext(), repository); + return new GitBrowseCommand(createContext(), repository, null); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java index 0418bc3e61..eea8bc0017 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java @@ -39,12 +39,18 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import sonia.scm.NotFoundException; 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.IOException; import java.io.InputStream; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitCatCommand}. @@ -136,7 +142,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { CatCommandRequest request = new CatCommandRequest(); 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('\n', catResultStream.read()); @@ -145,13 +151,38 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { 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 { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { - new GitCatCommand(createContext(), repository).getCatResult(request, + new GitCatCommand(createContext(), repository, null).getCatResult(request, baos); } finally diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 8f689e9664fc97179530c73aca4c969b1cfe962a..addda8609058d57eae3a0f91a0aa9e66b0a187d4 100644 GIT binary patch delta 4369 zcma)82~<G zMX*|1z$I<95^bxMDs@%_i^Io?<5H`(R;>zF?B~C{fC;wiFT=VacRKg(z0W=8T>6&M zewwmDNoDL@IjIacP~ZKW`2WEs>Y^`KPJ$7X%#xKc8sqWsv+G(B1gS(3gobdCsnZwC zjI!igrWH;|ABbP%;?w!S*}NQh+MJk9Ri->*N0 zExWVTT;|~+jtj)=wZ5y4RdP#jueqpLLl;zhm~-^Bs`%0>UC8F61E}No|6HQ-sH+>G zMHly^=azo^X}wQg?DX4re)`w(^d}R>m(LCSqbE;1uQKL*VfP8i1^SiGk8df<+n$hj zw(+*;d2SeEey4fpqZI4jcFoB6X&=q@JP+e<{O7vmt1}I=?sg^bxtLqGj{dw6TY_tz z90}U9Wp;<9&SQ0hdCM|k?Prz99EQxt^YZn^tF5Bvhn`SO$ru&h)iV7!NTOb=BeAbN zcr(Yr8C0NY)ZYjap-k#mjtzkA93HjdiHi3|=TZ|sEtAd9t{Pd*s%3A_Vn>nP%oh(=r!K}cSM z6->HZB7SwTK7PkLX8&=v^z9a6rK3p+SVaXh3M~tY#l!}UMk%w|l(KxeN~6G)Qj5i! zPkdQq8l_aJk!oZG7Mt4o%B-5e3q_IsB{D^xL?MyLrRKBAtwMuPAUKs0Au@<;rmV-G zw#}dL=u*J3Yn>ks-rOmCXTyQv^)mCWnomo$2fIyR{>8ZW(4`>8PX58F&O+lxk8~{~ zQ(NHCJ*0N><7fN%C0jOM>&6vdWYwNHf(#m3&T89U)ZTNo?&h4EN7kwY&wlV&9h>~$ zf)8e-Vl!J#z1;E1qi65_@#28@jh^^1TaKN%jDJF#-?nO@yx8wt+B4ekZLDnXmIKXC z=NWe;P2YRVOS?Mlq`h-Kii{YV@e}FjkM+Z0u51Y1rf~B2sN;eW9G+e#ZH1pR84#tQ z!G`D|)ZRfB?2ArycD4yw5E|nP5qP+3wg`95+M94T#WnnVR4R3&!)z1I0?S*cV7i6_ zOJ#0mYh`X`>2hBP5WAVp6Zg(Gh_65-Kf-01rF2_1S=qa+3HO7;$`7oWpv zNg>OmEYd$p@0Kr<6TabS?*2h+$k!BV)VLha&sWMVR;xxXx2XyW)hemPQlP-qvO^FDY~Jtoa+B z$?#Htj!$1oaO`OAJQEu%QXIF+B{G||z@o9r6l$fqK#E&6QmI^H$yX5q6j&vAzjydr z+*wTVI;v&-5Ih7K-Fje!PiC-*{UU&|qOdM$>6(5cnwhFh}i1(MRewG8E61dBJ+VXCJA7 z@Et$N`3we!9fVSXVaMx=k-KfR$$PaV0u8jv#{o^qfiG2VD=w;BzA*46ig?@5$q>Tl zgCW|xFNyRf9MbV^uE6L>;l#sck%CkoPOyDVh~a4Ng2-%QTxpZYthhy~(kO9-RW2=5 zYZNL)fmJHa$5j@UtWa*lrLP3}n?$gf^5Ht|3`{yiZ|@ZB@ewr?wzkvb*O}hI)(Xp( zmslAbA3Uw)$JGnN_l;I>?eTy3`1ae4ErIbrOe(4$U0F=C2_#;4#VvhAQAkMG)`r%6 zM!@Ia6`!kp&!AUsJvmc1L|C@bZ`kTgo_XW$aQ|V)XU1J)Fa72BCo9C~E~Er-R4cdq z_VAC#=kM9?`z~eELcHdc#thJfm8iSNyfjK)u)aAo@5{Nn2hM#sd&|Y0Ig=vVmzujg z_jH)0@P1ULQ?46P6Y0`?CwI(iuYw{A^2AhlC`fX?)S}2RND(B%t_T5Kj|uDJpG>i# zv0`UGBUbBtAe&=@$wVye)d9?LQsuG4Z5gT|1Yg z3JA3v&5uZB|1UW^j(F{n$ZT(>E6tQlLAjVS!B%BY9VfWo2uY*wd-W?4L8}d=k`A@a z5CElGF13`X8zC|=99pzo$|ZQ96@ibQi?T2f>kWh7r6S1mIkqA(_$F{6&OjrIB=(?2 zV$@M+K(Jw?+>x#cXsxDiYYVCSL@ZR#Y^zSPWNnQZVVLu$M4dIvjq~#gbh-fsN+*!-NDa?9j8(RY7o6 zFC@+$0MtY-x-1w%6UAg4$jbKidct!eB}*y?+_zqbOYa9)J;H5$ZwclOdl-3;ZY&pD zJ_3QF5DxgIbI7sJ#&QRjhW74;jyNWWa+yGnr@`GZT&!{=0^DF0@W%2I95?7+_Bjcs z5UkfMzsLXU4O~T_n*dJdK4f4sw}rVhA}cbtRk?qy#`@%?Afe0e^xyiXwVB%iB-PCZ zZ!|~%w@gONDnOu7s(=}09#P3~QFa6j%ratAA`obfR>0aU9&pmQ=;a6q%`l?Dk+3jh zA!-%EuS9Q$5MnYjV2zoJUKPO?W+OTx3jQ=_KuR(f-5LeyMk9JF3TllRgtyb_Uj3x= zlR#6V9BvWDvn4=HH)2~Q2+UI|U{yNLMOK^CMHX3}xM3m5>R+0;ljSe#xAjd6mA(U- zQgC#y3|^)f(HAlpH_i$4Z8_vlG-BV$5oppVKsKHSLsPh@S^-DK8PTN*7?zqrj(n{I zMT!yYRw5vYRKWfe9vm=m(Yq?>Fc>kB8iC+&1r#KWMt7;9Cdr6BC5C<`h9Z)klpoVT zbea+M#9?h(2D~(K(Ze`!Oh$CjD41=^a8a~rlnWFQ8?DyDmDGi3SUk*1HlnlQp)olF4B1>XD*>iwr=lMxz`ksa>&Cu1#62as z3_4=;rW`IhQ3v~SGO;r{1TM$1$f#t}(;z;Fi;dAE@F;dDEXv_o9C4z5s&j&KMy37w eey98;Aib9KOvxSt964kn@mC&%Ad)eT-v0oNcim?I delta 2334 zcmY*Z3s6+o89saNU6ggXU6#mWSue=T-Q~^hvOsu0%6l)0u*>_gtUzeMf~JlENm|iK zJ66+gMiVQ>b|j9m8OLl)n?_8SOj1juZIeoDh=Xy))F#E6v}w}R#GbtiEY8l{z4t%= z`Of!!|9}2#e^C7OH;O}6{?8X~wmh9($xpw={`WmAe-#<=9JvkPVa& z715L4{v87;t}a)fyJ2e};f}Js-u*jwts@HlHb;p3IREtT4{+&f{(i_?d!v86|I+qd z#;4x;crcqAyO;XV*H0WdK55J0rfxL<_x_Fcn`vLx9l5)?{{^FN z;NUY&ou^8=-yNH6x%+CSc6c#l^BZRlUCr2D^?cpS6Y-y%+wryatMI)|=YzhvSUPao z{mI&=7B*a(pLurEw-l5wo4luN-CgSWQrV$^u13I#&{@QCF*1gylIv2e z2Ej8?e@Cn_6z}s|ycy~RSHWo1ASgC$`I~pK3ZN`Do{wN2t>k@bSboGKUX?uS<;^92-DgrHEl7KwRw{sUHf zjOD$K&od2}u=&{7Y%eS`!MC<|aXLkh+G6!e?6G3IVjj~*9TtlBqO3`aCz4e7(5hZy zs={Fw3(L$^OfO|=uqSf|Y>eA{hMLa5w!(oo+xdAfhw+pM44H#h(Aq3&dfA`1&tm6K zd-JYYZY$1X#u_PPZ1btQCCo{y5T}al{AC|v7=+>iAuH3D7ryX-`a3C0h=r-w;cnGp zAv=^F^}F*53&rJ(GXo7P!Uv77;C8Zse(ooh%wU#|GvQU69?pEfQF9$yAm3+09R8hO zf?Fm%{*k4|+i_m21w>@O%u_sMN_U`?uX7&9xtX&KXv78!cEl zGy;Ds2ws9v;nRoxHSl6owF^kdeS?wKim+sTa$npmU&i->(zNUUZtbMVfbxTC2myeXtxgcD$CeF z&~Y=71iuK4L{4=KLMp_-Nj)L`gFWtDu-C?+y+Wi^#GZWGu0<1RrMS&k07xA~T`y>mo{T6$=R519}R zSuFbX0~`9>T|Fc0swfE;;jLa6V-tXDb}>*9y?k7uI~41sD1_TZZp%i3BUV*Zk8x#z@Fxvbi#lynk8=5KoFc|!Q(A4 zIMprEV@CY0TcRHs5$aajyfNc#T{f-r54HDgI#gzt!tZaWK9_^Iq zJDIrH>C36GVLkKmfDPN)9rA%4S-9$uxRY4~*UT2QIAdh^=xnIANSr&HATQB^Lt6@k z(>Z?q)@&$Rj962z{9wHkV-3WWgn- zwYXBGiFxR*tffctaIP|5ZucRVNayl!&}FB(d@Q)Km@vI&vFNAygvdQEx2)VKqFbcz xEN5v=w^4{tvmNBvt?b From 5b4d1d6003416ff9cf05ab46eaf416b3c1e424eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 25 Sep 2019 08:31:42 +0200 Subject: [PATCH 03/80] Get correct sizes for lfs files --- .../java/sonia/scm/repository/GitUtil.java | 20 ++++++++++++ .../scm/repository/spi/GitBrowseCommand.java | 27 ++++++++++++---- .../scm/repository/spi/GitCatCommand.java | 32 +++++++++---------- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 7175d3b646..9bc40f409a 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -42,7 +42,10 @@ import com.google.common.collect.Multimap; import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; 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.lfs.LfsPointer; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; 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.treewalk.TreeWalk; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.LfsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; @@ -65,10 +69,12 @@ import sonia.scm.web.GitUserAgentProvider; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import static java.util.Optional.empty; import static java.util.Optional.of; //~--- JDK imports ------------------------------------------------------------ @@ -731,6 +737,20 @@ public final class GitUtil } } + public static Optional 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(); + } + } + //~--- methods -------------------------------------------------------------- /** diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 1bc59f8e1b..5ec69cccdd 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -38,6 +38,7 @@ package sonia.scm.repository.spi; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -57,6 +58,8 @@ import sonia.scm.repository.GitSubModuleParser; import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.SubRepository; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; import sonia.scm.util.Util; import sonia.scm.web.lfs.LfsBlobStoreFactory; @@ -65,6 +68,7 @@ import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -170,7 +174,7 @@ public class GitBrowseCommand extends AbstractGitCommand * @throws IOException */ private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) + BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { FileObject file = new FileObject(); @@ -198,7 +202,6 @@ public class GitBrowseCommand extends AbstractGitCommand ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); file.setDirectory(loader.getType() == Constants.OBJ_TREE); - file.setLength(loader.getSize()); // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) @@ -206,6 +209,16 @@ public class GitBrowseCommand extends AbstractGitCommand logger.trace("fetch last commit for {} at {}", path, revId.getName()); RevCommit commit = getLatestCommit(repo, revId, path); + Optional 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) { file.setLastModified(GitUtil.getCommitTime(commit)); @@ -235,7 +248,7 @@ public class GitBrowseCommand extends AbstractGitCommand * @return */ private RevCommit getLatestCommit(org.eclipse.jgit.lib.Repository repo, - ObjectId revId, String path) + ObjectId revId, String path) { RevCommit result = null; RevWalk walk = null; @@ -342,7 +355,7 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { + BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { String[] pathElements = request.getPath().split("/"); int currentDepth = 0; int limit = pathElements.length; @@ -367,7 +380,7 @@ public class GitBrowseCommand extends AbstractGitCommand @SuppressWarnings("unchecked") private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo, - ObjectId revision) + ObjectId revision) throws IOException { if (logger.isDebugEnabled()) { @@ -392,7 +405,7 @@ public class GitBrowseCommand extends AbstractGitCommand } private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, - ObjectId revId, String path) + ObjectId revId, String path) throws IOException { Map subRepositories = subrepositoryCache.get(revId); @@ -413,7 +426,7 @@ public class GitBrowseCommand extends AbstractGitCommand } //~--- fields --------------------------------------------------------------- - + /** sub repository cache */ private final Map> subrepositoryCache = Maps.newHashMap(); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 21f4b942df..c9f33d84f4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -50,6 +50,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.web.lfs.LfsBlobStoreFactory; @@ -59,6 +60,7 @@ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Optional; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -126,29 +128,27 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { treeWalk.setFilter(PathFilter.create(path)); if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { - Attributes attributes = LfsFactory.getAttributesForPath(repo, path, entry); - - Attribute filter = attributes.get("filter"); - if (filter != null && "lfs".equals(filter.getValue())) { - return loadFromLfsStore(repo, treeWalk, revWalk); + Optional lfsPointer = GitUtil.getLfsPointer(repo, path, entry, treeWalk); + if (lfsPointer.isPresent()) { + return loadFromLfsStore(treeWalk, revWalk, lfsPointer.get()); + } else { + return loadFromGit(repo, treeWalk, revWalk); } - - ObjectId blobId = treeWalk.getObjectId(0); - ObjectLoader loader = repo.open(blobId); - - return new GitObjectLoaderWrapper(loader, treeWalk, revWalk); } else { throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository)); } } - private Loader loadFromLfsStore(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException { + private Loader loadFromGit(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException { ObjectId blobId = treeWalk.getObjectId(0); - LfsPointer lfsPointer; - try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) { - lfsPointer = LfsPointer.parseLfsPointer(is); - } - Blob blob = lfsBlobStoreFactory.getLfsBlobStore(repository).get(lfsPointer.getOid().getName()); + 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); From 3d27938e80031bb7ce65c02bb7586f46df90566b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 25 Sep 2019 12:15:30 +0200 Subject: [PATCH 04/80] Postpone writing to output stream in diff command Without this, the gzip filter would write the first bytes to the response output stream, before the diff command is triggered and potential exceptions may be thrown. When exceptions are thrown too late, filters like the GZip filter may already have sent bytes to the response stream. Afterwards this cannot be undone and the response created by an exception mapper may not be valid anymore. --- .../repository/api/DiffCommandBuilder.java | 46 ++++++----------- .../sonia/scm/repository/spi/DiffCommand.java | 8 +-- .../java/sonia/scm/repository/GitUtil.java | 8 ++- .../scm/repository/spi/GitDiffCommand.java | 28 +++++------ .../repository/spi/GitDiffCommandTest.java | 10 ++-- .../scm/repository/spi/HgDiffCommand.java | 38 ++++++-------- .../scm/repository/spi/SvnDiffCommand.java | 50 +++++++++---------- .../api/v2/resources/DiffRootResource.java | 17 +++---- .../v2/resources/IncomingRootResource.java | 14 +++--- .../api/v2/resources/DiffResourceTest.java | 10 ++-- .../resources/IncomingRootResourceTest.java | 8 +-- 11 files changed, 110 insertions(+), 127 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index 18d4e11a7f..03f4361083 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -40,7 +40,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.Feature; import sonia.scm.repository.spi.DiffCommand; -import sonia.scm.util.IOUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -103,16 +102,12 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder { + try (DiffFormatter formatter = new DiffFormatter(output)) { + formatter.setRepository(repository); + + for (DiffEntry e : diff.getEntries()) { + if (!e.getOldId().equals(e.getNewId())) { + formatter.format(e); + } } - } - formatter.flush(); - } + formatter.flush(); + } + }; } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index fd9c45be5c..ffe63bb233 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -44,7 +44,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest, output); + gitDiffCommand.getDiffResult(diffCommandRequest); assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); } @@ -54,7 +54,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest, output); + gitDiffCommand.getDiffResult(diffCommandRequest); assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); } @@ -65,7 +65,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { diffCommandRequest.setRevision("test-branch"); diffCommandRequest.setPath("a.txt"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest, output); + gitDiffCommand.getDiffResult(diffCommandRequest); assertEquals(DIFF_FILE_A, output.toString()); } @@ -76,7 +76,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { diffCommandRequest.setRevision("master"); diffCommandRequest.setAncestorChangeset("test-branch"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest, output); + gitDiffCommand.getDiffResult(diffCommandRequest); 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.setPath("a.txt"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest, output); + gitDiffCommand.getDiffResult(diffCommandRequest); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java index d705f0ac14..b81a381a14 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java @@ -39,13 +39,12 @@ import com.google.common.base.Strings; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import sonia.scm.repository.Repository; +import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.spi.javahg.HgDiffInternalCommand; import sonia.scm.web.HgUtil; -import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; //~--- JDK imports ------------------------------------------------------------ @@ -71,8 +70,7 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand //~--- get methods ---------------------------------------------------------- @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) - throws IOException + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) { com.aragost.javahg.Repository hgRepo = open(); @@ -86,26 +84,22 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand cmd.change(HgUtil.getRevision(request.getRevision())); - InputStream inputStream = null; + return output -> { + InputStream inputStream = null; - try - { + try { - if (!Strings.isNullOrEmpty(request.getPath())) - { - inputStream = cmd.stream(hgRepo.file(request.getPath())); + if (!Strings.isNullOrEmpty(request.getPath())) { + inputStream = cmd.stream(hgRepo.file(request.getPath())); + } else { + inputStream = cmd.stream(); + } + + ByteStreams.copy(inputStream, output); + + } finally { + Closeables.close(inputStream, true); } - else - { - inputStream = cmd.stream(); - } - - ByteStreams.copy(inputStream, output); - - } - finally - { - Closeables.close(inputStream, true); - } + }; } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java index 4aaa12e28f..3bbde02844 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnDiffCommand.java @@ -46,11 +46,10 @@ import org.tmatesoft.svn.core.wc.SVNRevision; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnUtil; +import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.util.Util; -import java.io.OutputStream; - //~--- JDK imports ------------------------------------------------------------ /** @@ -70,33 +69,34 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand { } @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) { + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) { logger.debug("create diff for {}", request); Preconditions.checkNotNull(request, "request is required"); - Preconditions.checkNotNull(output, "outputstream is required"); String path = request.getPath(); - SVNClientManager clientManager = null; - try { - SVNURL svnurl = context.createUrl(); - if (Util.isNotEmpty(path)) { - svnurl = svnurl.appendPath(path, true); + return output -> { + SVNClientManager clientManager = null; + try { + SVNURL svnurl = context.createUrl(); + if (Util.isNotEmpty(path)) { + svnurl = svnurl.appendPath(path, true); + } + clientManager = SVNClientManager.newInstance(); + SVNDiffClient diffClient = clientManager.getDiffClient(); + diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator())); + + long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository); + + diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT); + + diffClient.doDiff(svnurl, SVNRevision.HEAD, + SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev), + SVNDepth.INFINITY, false, output); + } catch (SVNException ex) { + throw new InternalRepositoryException(repository, "could not create diff", ex); + } finally { + SvnUtil.dispose(clientManager); } - clientManager = SVNClientManager.newInstance(); - SVNDiffClient diffClient = clientManager.getDiffClient(); - diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator())); - - long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository); - - diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT); - - diffClient.doDiff(svnurl, SVNRevision.HEAD, - SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev), - SVNDepth.INFINITY, false, output); - } catch (SVNException ex) { - throw new InternalRepositoryException(repository, "could not create diff", ex); - } finally { - SvnUtil.dispose(clientManager); - } + }; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java index 4d15b773cd..016c316500 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -4,6 +4,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -20,6 +21,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; 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 = 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); DiffFormat diffFormat = DiffFormat.valueOf(format); try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - StreamingOutput responseEntry = output -> { - repositoryService.getDiffCommand() - .setRevision(revision) - .setFormat(diffFormat) - .retrieveContent(output); - }; - return Response.ok(responseEntry) + DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand() + .setRevision(revision) + .setFormat(diffFormat) + .retrieveContent(); + return Response.ok((StreamingOutput) outputStreamConsumer::accept) .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision))) .build(); } } - } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java index 4c43485abd..cb27d9b5dc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java @@ -10,6 +10,7 @@ import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffFormat; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -138,14 +139,13 @@ public class IncomingRootResource { HttpUtil.checkForCRLFInjection(target); DiffFormat diffFormat = DiffFormat.valueOf(format); try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - StreamingOutput responseEntry = output -> - repositoryService.getDiffCommand() - .setRevision(source) - .setAncestorChangeset(target) - .setFormat(diffFormat) - .retrieveContent(output); + DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand() + .setRevision(source) + .setAncestorChangeset(target) + .setFormat(diffFormat) + .retrieveContent(); - return Response.ok(responseEntry) + return Response.ok((StreamingOutput) outputStreamConsumer::accept) .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, source))) .build(); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java index 01d5e22b53..33d4d7e9e1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -91,7 +91,7 @@ public class DiffResourceTest extends RepositoryTestBase { public void shouldGetDiffs() throws Exception { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); - when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {}); MockHttpRequest request = MockHttpRequest .get(DIFF_URL + "revision") .accept(VndMediaType.DIFF); @@ -123,7 +123,7 @@ public class DiffResourceTest extends RepositoryTestBase { public void shouldGet404OnMissingRevision() throws Exception { when(diffCommandBuilder.setRevision(anyString())).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 .get(DIFF_URL + "revision") @@ -139,7 +139,7 @@ public class DiffResourceTest extends RepositoryTestBase { public void shouldGet400OnCrlfInjection() throws Exception { when(diffCommandBuilder.setRevision(anyString())).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 .get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634") @@ -153,7 +153,7 @@ public class DiffResourceTest extends RepositoryTestBase { public void shouldGet400OnUnknownFormat() throws Exception { when(diffCommandBuilder.setRevision(anyString())).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 .get(DIFF_URL + "revision?format=Unknown") @@ -167,7 +167,7 @@ public class DiffResourceTest extends RepositoryTestBase { public void shouldAcceptDiffFormats() throws Exception { when(diffCommandBuilder.setRevision(anyString())).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( this::assertRequestOk diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java index b965c2f2c3..0c1f4235b9 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IncomingRootResourceTest.java @@ -171,7 +171,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); - when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {}); MockHttpRequest request = MockHttpRequest .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff") .accept(VndMediaType.DIFF); @@ -206,7 +206,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).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 .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.setAncestorChangeset(anyString())).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 .get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff") .accept(VndMediaType.DIFF); @@ -240,7 +240,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase { when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); when(diffCommandBuilder.setAncestorChangeset(anyString())).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 .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown") .accept(VndMediaType.DIFF); From 1b797e7de42c834fdc5a59ccecd0c844907ab3dd Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 25 Sep 2019 14:25:44 +0200 Subject: [PATCH 05/80] Fix unit test --- .../sonia/scm/repository/spi/GitDiffCommandTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index ffe63bb233..52932e83ae 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java @@ -44,7 +44,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); } @@ -54,7 +54,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { DiffCommandRequest diffCommandRequest = new DiffCommandRequest(); diffCommandRequest.setRevision("test-branch"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString()); } @@ -65,7 +65,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { diffCommandRequest.setRevision("test-branch"); diffCommandRequest.setPath("a.txt"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A, output.toString()); } @@ -76,7 +76,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase { diffCommandRequest.setRevision("master"); diffCommandRequest.setAncestorChangeset("test-branch"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); 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.setPath("a.txt"); ByteArrayOutputStream output = new ByteArrayOutputStream(); - gitDiffCommand.getDiffResult(diffCommandRequest); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); } } From e0e4db5583534d90483233341dd86e6e8e7932ff Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 25 Sep 2019 15:41:02 +0200 Subject: [PATCH 06/80] Fix diff for hg --- .../scm/repository/spi/HgDiffCommand.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java index b81a381a14..9e43e26014 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgDiffCommand.java @@ -72,19 +72,19 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand @Override public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) { - com.aragost.javahg.Repository hgRepo = open(); - - HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); - DiffFormat format = request.getFormat(); - - if (format == DiffFormat.GIT) - { - cmd.git(); - } - - cmd.change(HgUtil.getRevision(request.getRevision())); - return output -> { + com.aragost.javahg.Repository hgRepo = open(); + + HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); + DiffFormat format = request.getFormat(); + + if (format == DiffFormat.GIT) + { + cmd.git(); + } + + cmd.change(HgUtil.getRevision(request.getRevision())); + InputStream inputStream = null; try { From 689e3fa17e9c68849aa862fc531dbfb332af55e4 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 26 Sep 2019 15:42:21 +0200 Subject: [PATCH 07/80] do not use function as fetch argument, use the resulting object This should fix the login problem "Error: e._links.me is undefined" on older firefox browsers such as 56.0.2 --- scm-ui-components/packages/ui-components/src/apiclient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js index 2ad0cd1f9f..a656246c95 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.js @@ -46,7 +46,7 @@ export function createUrl(url: string) { class ApiClient { get(url: string): Promise { - return fetch(createUrl(url), applyFetchOptions).then(handleFailure); + return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure); } post(url: string, payload: any, contentType: string = "application/json") { From ac4eca7520a4f7db90149159513cd9d022ffeaf1 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 26 Sep 2019 16:51:26 +0200 Subject: [PATCH 08/80] Fetch exception when uninstall file could not be written --- .../sonia/scm/plugin/DefaultPluginManager.java | 9 +++++++-- .../scm/plugin/PluginDependencyTracker.java | 6 +++++- .../scm/plugin/DefaultPluginManagerTest.java | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index d54645f10d..1c636ffb17 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -193,9 +193,14 @@ public class DefaultPluginManager implements PluginManager { doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore()); dependencyTracker.removeInstalled(installed.getDescriptor()); - installed.setMarkedForUninstall(true); - createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME); + try { + createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME); + installed.setMarkedForUninstall(true); + } catch (RuntimeException e) { + dependencyTracker.addInstalled(installed.getDescriptor()); + throw e; + } if (restartAfterInstallation) { restart("plugin installation"); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java index a68b391b97..2609522555 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java @@ -33,6 +33,10 @@ class PluginDependencyTracker { } private void removeDependency(String from, String to) { - plugins.get(to).remove(from); + Collection dependencies = plugins.get(to); + if (dependencies == null) { + throw new NullPointerException("inverse dependencies not found for " + to); + } + dependencies.remove(from); } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 057a05eb79..ad196b8ca3 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -358,6 +358,24 @@ class DefaultPluginManagerTest { 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 void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) { InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); From 3145b751c690ffadce2242c99ea5d3714502dec4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 26 Sep 2019 17:50:54 +0200 Subject: [PATCH 09/80] Add cancel method to remove install and uninstall files --- .../java/sonia/scm/plugin/PluginManager.java | 2 + .../scm/plugin/DefaultPluginManager.java | 38 ++++++++++--------- .../plugin/PendingPluginUninstallation.java | 31 +++++++++++++++ .../scm/plugin/DefaultPluginManagerTest.java | 24 ++++++++++++ 4 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index 3d0ea94536..1e787d24c3 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -93,4 +93,6 @@ public interface PluginManager { * Install all pending plugins and restart the scm context. */ void executePendingAndRestart(); + + void cancelInstallations(); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 1c636ffb17..c03cfdab84 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -44,8 +44,8 @@ import sonia.scm.lifecycle.RestartEvent; import sonia.scm.version.Version; import javax.inject.Inject; -import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -72,7 +72,8 @@ public class DefaultPluginManager implements PluginManager { private final PluginLoader loader; private final PluginCenter center; private final PluginInstaller installer; - private final Collection pendingQueue = new ArrayList<>(); + private final Collection pendingInstallQueue = new ArrayList<>(); + private final Collection pendingUninstallQueue = new ArrayList<>(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); @Inject @@ -106,7 +107,7 @@ public class DefaultPluginManager implements PluginManager { } private Optional getPending(String name) { - return pendingQueue + return pendingInstallQueue .stream() .map(PendingPluginInstallation::getPlugin) .filter(filterByName(name)) @@ -179,7 +180,7 @@ public class DefaultPluginManager implements PluginManager { if (restartAfterInstallation) { restart("plugin installation"); } else { - pendingQueue.addAll(pendingInstallations); + pendingInstallQueue.addAll(pendingInstallations); updateMayUninstallFlag(); } } @@ -192,15 +193,8 @@ public class DefaultPluginManager implements PluginManager { .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore()); - dependencyTracker.removeInstalled(installed.getDescriptor()); - try { - createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME); - installed.setMarkedForUninstall(true); - } catch (RuntimeException e) { - dependencyTracker.addInstalled(installed.getDescriptor()); - throw e; - } + markForUninstall(installed); if (restartAfterInstallation) { restart("plugin installation"); @@ -220,18 +214,22 @@ public class DefaultPluginManager implements PluginManager { && dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName()); } - private void createMarkerFile(InstalledPlugin plugin, String markerFile) { + private void markForUninstall(InstalledPlugin plugin) { + dependencyTracker.removeInstalled(plugin.getDescriptor()); try { - Files.createFile(plugin.getDirectory().resolve(markerFile)); - } catch (IOException e) { - throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + markerFile, e); + Path file = Files.createFile(plugin.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); + pendingUninstallQueue.add(new PendingPluginUninstallation(plugin, file)); + 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 public void executePendingAndRestart() { PluginPermissions.manage().check(); - if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) { + if (!pendingInstallQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) { restart("execute pending plugin changes"); } } @@ -274,4 +272,10 @@ public class DefaultPluginManager implements PluginManager { private boolean isUpdatable(String name) { return getAvailable(name).isPresent() && !getPending(name).isPresent(); } + + @Override + public void cancelInstallations() { + pendingUninstallQueue.forEach(PendingPluginUninstallation::cancel); + pendingInstallQueue.forEach(PendingPluginInstallation::cancel); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java new file mode 100644 index 0000000000..e5481fb292 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java @@ -0,0 +1,31 @@ +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); + } catch (IOException ex) { + throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation of plugin " + name, ex); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index ad196b8ca3..c761034ac1 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -20,6 +20,8 @@ import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Optional; @@ -445,6 +447,28 @@ class DefaultPluginManagerTest { 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)); + + 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.cancelInstallations(); + + assertThat(mailPluginPath.resolve("uninstall")).doesNotExist(); + verify(gitPendingPluginInformation).cancel(); + } } @Nested From 2519c415bf7b2ad7aa2ba273990e7500379d2156 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 27 Sep 2019 11:40:06 +0200 Subject: [PATCH 10/80] Set uninstalled marker on cancel --- .../java/sonia/scm/plugin/PendingPluginUninstallation.java | 1 + .../test/java/sonia/scm/plugin/DefaultPluginManagerTest.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java index e5481fb292..8d5a44d60a 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java @@ -24,6 +24,7 @@ class PendingPluginUninstallation { LOG.info("cancel uninstallation of plugin {}", name); try { Files.delete(uninstallFile); + plugin.setMarkedForUninstall(false); } catch (IOException ex) { throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation of plugin " + name, ex); } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index c761034ac1..520fcbbbef 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -32,6 +32,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; @@ -455,6 +456,8 @@ class DefaultPluginManagerTest { Files.createDirectories(mailPluginPath); when(mailPlugin.getDirectory()).thenReturn(mailPluginPath); when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + ArgumentCaptor uninstallCaptor = ArgumentCaptor.forClass(Boolean.class); + doNothing().when(mailPlugin).setMarkedForUninstall(uninstallCaptor.capture()); AvailablePlugin git = createAvailable("scm-git-plugin"); when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); @@ -468,6 +471,8 @@ class DefaultPluginManagerTest { assertThat(mailPluginPath.resolve("uninstall")).doesNotExist(); verify(gitPendingPluginInformation).cancel(); + Boolean lasUninstallMarkerSet = uninstallCaptor.getAllValues().get(uninstallCaptor.getAllValues().size() - 1); + assertThat(lasUninstallMarkerSet).isFalse(); } } From 3b34cb527840e8354e820ce7cf2e0098b7846344 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 27 Sep 2019 11:46:14 +0200 Subject: [PATCH 11/80] Rename method and check permission --- scm-core/src/main/java/sonia/scm/plugin/PluginManager.java | 2 +- .../main/java/sonia/scm/plugin/DefaultPluginManager.java | 3 ++- .../java/sonia/scm/plugin/DefaultPluginManagerTest.java | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index 1e787d24c3..964ebf605b 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -94,5 +94,5 @@ public interface PluginManager { */ void executePendingAndRestart(); - void cancelInstallations(); + void cancelPending(); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index c03cfdab84..ed5e7f78db 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -274,7 +274,8 @@ public class DefaultPluginManager implements PluginManager { } @Override - public void cancelInstallations() { + public void cancelPending() { + PluginPermissions.manage().check(); pendingUninstallQueue.forEach(PendingPluginUninstallation::cancel); pendingInstallQueue.forEach(PendingPluginInstallation::cancel); } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 520fcbbbef..84bf843ca2 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -467,7 +467,7 @@ class DefaultPluginManagerTest { manager.install("scm-git-plugin", false); manager.uninstall("scm-ssh-plugin", false); - manager.cancelInstallations(); + manager.cancelPending(); assertThat(mailPluginPath.resolve("uninstall")).doesNotExist(); verify(gitPendingPluginInformation).cancel(); @@ -529,5 +529,9 @@ class DefaultPluginManagerTest { assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart()); } + @Test + void shouldThrowAuthorizationExceptionsForCancelPending() { + assertThrows(AuthorizationException.class, () -> manager.cancelPending()); + } } } From fd4070b1b1ccec0265d82b73bc9a83a8c0e27a27 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 27 Sep 2019 11:49:14 +0200 Subject: [PATCH 12/80] Add rest method --- .../v2/resources/PendingPluginResource.java | 15 +- .../resources/PendingPluginResourceTest.java | 202 ++++++------------ 2 files changed, 81 insertions(+), 136 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java index 3997a5e7c5..17fff25f16 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java @@ -8,7 +8,6 @@ import de.otto.edison.hal.Links; import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginManager; -import sonia.scm.plugin.PluginPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -46,8 +45,6 @@ public class PendingPluginResource { }) @Produces(VndMediaType.PLUGIN_COLLECTION) public Response getPending() { - PluginPermissions.manage().check(); - List pending = pluginManager .getAvailable() .stream() @@ -106,8 +103,18 @@ public class PendingPluginResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response executePending() { - PluginPermissions.manage().check(); pluginManager.executePendingAndRestart(); 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(); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java index e5587b78cd..e1b0325cb2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java @@ -1,16 +1,11 @@ package sonia.scm.api.v2.resources; import com.google.inject.util.Providers; -import org.apache.shiro.ShiroException; -import org.apache.shiro.subject.Subject; -import org.apache.shiro.util.ThreadContext; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,8 +19,6 @@ import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; @@ -34,11 +27,8 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -54,9 +44,6 @@ class PendingPluginResourceTest { @Mock PluginDtoMapper mapper; - @Mock - Subject subject; - @InjectMocks PendingPluginResource pendingPluginResource; @@ -65,7 +52,6 @@ class PendingPluginResourceTest { @BeforeEach void prepareEnvironment() { dispatcher = MockDispatcherFactory.createDispatcher(); - dispatcher.getProviderFactory().register(new PermissionExceptionMapper()); PluginRootResource pluginRootResource = new PluginRootResource(null, null, Providers.of(pendingPluginResource)); dispatcher.getRegistry().addSingletonResource(pluginRootResource); } @@ -74,141 +60,93 @@ class PendingPluginResourceTest { void mockMapper() { lenient().when(mapper.mapAvailable(any())).thenAnswer(invocation -> { PluginDto dto = new PluginDto(); - dto.setName(((AvailablePlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName()); + dto.setName(((AvailablePlugin) invocation.getArgument(0)).getDescriptor().getInformation().getName()); return dto; }); lenient().when(mapper.mapInstalled(any(), any())).thenAnswer(invocation -> { PluginDto dto = new PluginDto(); - dto.setName(((InstalledPlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName()); + dto.setName(((InstalledPlugin) invocation.getArgument(0)).getDescriptor().getInformation().getName()); return dto; }); } - @Nested - class withAuthorization { + @Test + void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); + when(availablePlugin.isPending()).thenReturn(false); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); - @BeforeEach - void bindSubject() { - ThreadContext.bind(subject); - doNothing().when(subject).checkPermission("plugin:manage"); - } + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); - @AfterEach - void unbindSubject() { - ThreadContext.unbindSubject(); - } - - @Test - void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); - when(availablePlugin.isPending()).thenReturn(false); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); - - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); - assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); - } - - @Test - void shouldGetPendingAvailablePluginListWithInstallLink() 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"); - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); - assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); - } - - @Test - void shouldGetPendingUpdatePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); - when(availablePlugin.isPending()).thenReturn(true); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); - InstalledPlugin installedPlugin = createInstalledPlugin("available-plugin"); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); - - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\""); - assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); - } - - @Test - void shouldGetPendingUninstallPluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - when(pluginManager.getAvailable()).thenReturn(emptyList()); - InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); - when(installedPlugin.isMarkedForUninstall()).thenReturn(true); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); - - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"uninstall\":[{\"name\":\"uninstalled-plugin\""); - assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); - } - - @Test - void shouldExecutePendingPlugins() throws URISyntaxException { - MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute"); - - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - verify(pluginManager).executePendingAndRestart(); - } + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); + assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); } - @Nested - class WithoutAuthorization { + @Test + void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); + when(availablePlugin.isPending()).thenReturn(true); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); - @BeforeEach - void bindSubject() { - ThreadContext.bind(subject); - doThrow(new ShiroException()).when(subject).checkPermission("plugin:manage"); - } + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); - @AfterEach - void unbindSubject() { - ThreadContext.unbindSubject(); - } - - @Test - void shouldNotListPendingPlugins() throws URISyntaxException { - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); - verify(pluginManager, never()).executePendingAndRestart(); - } - - @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(); - } + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); + assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); } - static class PermissionExceptionMapper implements ExceptionMapper { + @Test + void shouldGetPendingUpdatePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); + when(availablePlugin.isPending()).thenReturn(true); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + InstalledPlugin installedPlugin = createInstalledPlugin("available-plugin"); + when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); - @Override - public Response toResponse(ShiroException exception) { - return Response.status(401).entity(exception.getMessage()).build(); - } + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\""); + assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); + } + + @Test + void shouldGetPendingUninstallPluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + when(pluginManager.getAvailable()).thenReturn(emptyList()); + InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); + when(installedPlugin.isMarkedForUninstall()).thenReturn(true); + when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"uninstall\":[{\"name\":\"uninstalled-plugin\""); + assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); + } + + @Test + void shouldExecutePendingPlugins() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute"); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + verify(pluginManager).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(); } private AvailablePlugin createAvailablePlugin(String name) { From 52a9429ef5e511453443458af53562888863fdc9 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 27 Sep 2019 12:47:41 +0200 Subject: [PATCH 13/80] Add cancel link --- .../v2/resources/PendingPluginResource.java | 7 +- .../scm/api/v2/resources/ResourceLinks.java | 4 + .../resources/PendingPluginResourceTest.java | 202 ++++++++++++------ 3 files changed, 144 insertions(+), 69 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java index 17fff25f16..7d4663829e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java @@ -8,6 +8,7 @@ import de.otto.edison.hal.Links; import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -68,8 +69,12 @@ public class PendingPluginResource { List updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); List uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); - if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) { + if ( + PluginPermissions.manage().isPermitted() && + (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) + ) { linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending())); + linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending())); } Embedded.Builder embedded = Embedded.embeddedBuilder(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index c36cfc09ad..416af2e478 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -739,6 +739,10 @@ class ResourceLinks { return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href(); } + String cancelPending() { + return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("cancelPending").parameters().href(); + } + String self() { return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href(); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java index e1b0325cb2..da6e1b65d7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java @@ -1,11 +1,16 @@ package sonia.scm.api.v2.resources; import com.google.inject.util.Providers; +import org.apache.shiro.ShiroException; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -19,6 +24,8 @@ import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; @@ -44,6 +51,9 @@ class PendingPluginResourceTest { @Mock PluginDtoMapper mapper; + @Mock + Subject subject; + @InjectMocks PendingPluginResource pendingPluginResource; @@ -52,6 +62,7 @@ class PendingPluginResourceTest { @BeforeEach void prepareEnvironment() { dispatcher = MockDispatcherFactory.createDispatcher(); + dispatcher.getProviderFactory().register(new PermissionExceptionMapper()); PluginRootResource pluginRootResource = new PluginRootResource(null, null, Providers.of(pendingPluginResource)); dispatcher.getRegistry().addSingletonResource(pluginRootResource); } @@ -70,83 +81,138 @@ class PendingPluginResourceTest { }); } - @Test - void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); - when(availablePlugin.isPending()).thenReturn(false); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + @Nested + class withAuthorization { - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + lenient().when(subject.isPermitted("plugin:manage")).thenReturn(true); + } + + @AfterEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); + when(availablePlugin.isPending()).thenReturn(false); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); + assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); + } + + @Test + void shouldGetPendingAvailablePluginListWithInstallAndCancelLink() 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"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); + assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); + assertThat(response.getContentAsString()).contains("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}"); + } + + @Test + void shouldGetPendingUpdatePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); + when(availablePlugin.isPending()).thenReturn(true); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + InstalledPlugin installedPlugin = createInstalledPlugin("available-plugin"); + when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\""); + assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); + } + + @Test + void shouldGetPendingUninstallPluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + when(pluginManager.getAvailable()).thenReturn(emptyList()); + InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); + when(installedPlugin.isMarkedForUninstall()).thenReturn(true); + when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"uninstall\":[{\"name\":\"uninstalled-plugin\""); + assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); + } + + @Test + void shouldExecutePendingPlugins() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute"); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + verify(pluginManager).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(); + } - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); - assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); } - @Test - void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); - when(availablePlugin.isPending()).thenReturn(true); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + @Nested + class WithoutAuthorization { - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + when(subject.isPermitted("plugin:manage")).thenReturn(false); + } - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); - assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); + @AfterEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + 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"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + 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 shouldGetPendingUpdatePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); - when(availablePlugin.isPending()).thenReturn(true); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); - InstalledPlugin installedPlugin = createInstalledPlugin("available-plugin"); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + static class PermissionExceptionMapper implements ExceptionMapper { - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\""); - assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); - } - - @Test - void shouldGetPendingUninstallPluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - when(pluginManager.getAvailable()).thenReturn(emptyList()); - InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); - when(installedPlugin.isMarkedForUninstall()).thenReturn(true); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); - - MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"uninstall\":[{\"name\":\"uninstalled-plugin\""); - assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}"); - } - - @Test - void shouldExecutePendingPlugins() throws URISyntaxException { - MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute"); - - dispatcher.invoke(request, response); - - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - verify(pluginManager).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(); + @Override + public Response toResponse(ShiroException exception) { + return Response.status(401).entity(exception.getMessage()).build(); + } } private AvailablePlugin createAvailablePlugin(String name) { From 32cb67f92e9b81c065c3286c592df42197a3f56b Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 27 Sep 2019 14:05:19 +0200 Subject: [PATCH 14/80] implement MultiPluginActions for UpdateAll and CancelPending --- .../packages/ui-types/src/Plugin.js | 1 + .../components/ExecutePendingAction.js | 68 ---------- .../plugins/components/MultiPluginAction.js | 90 +++++++++++++ ...dingModal.js => MultiPluginActionModal.js} | 125 ++++++++++++++++-- .../plugins/containers/PluginsOverview.js | 43 ++++-- 5 files changed, 236 insertions(+), 91 deletions(-) delete mode 100644 scm-ui/src/admin/plugins/components/ExecutePendingAction.js create mode 100644 scm-ui/src/admin/plugins/components/MultiPluginAction.js rename scm-ui/src/admin/plugins/components/{ExecutePendingModal.js => MultiPluginActionModal.js} (57%) diff --git a/scm-ui-components/packages/ui-types/src/Plugin.js b/scm-ui-components/packages/ui-types/src/Plugin.js index c7612a8bf8..f5a621e1ff 100644 --- a/scm-ui-components/packages/ui-types/src/Plugin.js +++ b/scm-ui-components/packages/ui-types/src/Plugin.js @@ -17,6 +17,7 @@ export type Plugin = { }; export type PluginCollection = Collection & { + _links: Links, _embedded: { plugins: Plugin[] | string[] } diff --git a/scm-ui/src/admin/plugins/components/ExecutePendingAction.js b/scm-ui/src/admin/plugins/components/ExecutePendingAction.js deleted file mode 100644 index 6c8d407205..0000000000 --- a/scm-ui/src/admin/plugins/components/ExecutePendingAction.js +++ /dev/null @@ -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 { - 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 ( - - ); - } - return null; - }; - - render() { - const { t } = this.props; - return ( - <> - {this.renderModal()} - + icon={sideBySide ? "align-left" : "columns"} + label={t(sideBySide ? "diff.combined" : "diff.sideBySide")} + reducedMobile={true} + /> {fileControls} From b96087b0b0895a4ed167194049ad02bab2ac45a1 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 30 Sep 2019 15:08:14 +0200 Subject: [PATCH 23/80] fix collapsible option --- .../packages/ui-components/src/repos/DiffFile.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index dd0ce9d84b..f03c804338 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -69,7 +69,7 @@ const styles = { type Props = DiffObjectProps & { file: File, - collapsible: true, + collapsible: boolean, // context props classes: any, @@ -82,6 +82,10 @@ type State = { }; class DiffFile extends React.Component { + static defaultProps = { + collapsible: true + }; + constructor(props: Props) { super(props); this.state = { @@ -91,7 +95,7 @@ class DiffFile extends React.Component { } toggleCollapse = () => { - if (this.props.collapsable) { + if (this.props.collapsible) { this.setState(state => ({ collapsed: !state.collapsed })); From 765667bf2050edeb514d531fda8d5e443597d4c3 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 30 Sep 2019 15:11:45 +0200 Subject: [PATCH 24/80] use icon component instead --- .../packages/ui-components/src/repos/DiffFile.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index f03c804338..dd48643beb 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -13,6 +13,7 @@ import classNames from "classnames"; import { translate } from "react-i18next"; import { Button, ButtonGroup } from "../buttons"; import Tag from "../Tag"; +import Icon from "../Icon"; const styles = { panel: { @@ -177,7 +178,7 @@ class DiffFile extends React.Component { ) { return ( <> - {file.oldPath} {file.newPath} + {file.oldPath} {file.newPath} ); } else if (file.type === "delete") { @@ -245,12 +246,12 @@ class DiffFile extends React.Component { const viewType = sideBySide ? "split" : "unified"; let body = null; - let icon = "fa fa-angle-right"; + let icon = "angle-right"; if (!collapsed) { const fileAnnotations = fileAnnotationFactory ? fileAnnotationFactory(file) : null; - icon = "fa fa-angle-down"; + icon = "angle-down"; body = (
{fileAnnotations} @@ -263,7 +264,7 @@ class DiffFile extends React.Component {
); } - const collapseIcon = collapsible ? : null; + const collapseIcon = collapsible ? : null; const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) From bef64e9831c4e3ca321de1e6f89733b963ce8b63 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 30 Sep 2019 15:29:47 +0200 Subject: [PATCH 25/80] add defaultCollapse option to force toggling all DiffFile components by once, remove toggle option and icon for binary files --- .../ui-components/src/repos/DiffFile.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index dd48643beb..8dcdf0a640 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -70,7 +70,7 @@ const styles = { type Props = DiffObjectProps & { file: File, - collapsible: boolean, + defaultCollapse: boolean, // context props classes: any, @@ -84,19 +84,20 @@ type State = { class DiffFile extends React.Component { static defaultProps = { - collapsible: true + defaultCollapse: false }; constructor(props: Props) { super(props); this.state = { - collapsed: false, + collapsed: this.props.defaultCollapse, sideBySide: false }; } toggleCollapse = () => { - if (this.props.collapsible) { + const { file } = this.props; + if(file && !file.isBinaray) { this.setState(state => ({ collapsed: !state.collapsed })); @@ -178,7 +179,8 @@ class DiffFile extends React.Component { ) { return ( <> - {file.oldPath} {file.newPath} + {file.oldPath} {" "} + {file.newPath} ); } else if (file.type === "delete") { @@ -238,7 +240,6 @@ class DiffFile extends React.Component { file, fileControlFactory, fileAnnotationFactory, - collapsible, classes, t } = this.props; @@ -264,7 +265,9 @@ class DiffFile extends React.Component { ); } - const collapseIcon = collapsible ? : null; + const collapseIcon = !file.isBinary ? ( + + ) : null; const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) From 0a5575c2fd5e26eb7cf9e9f27990ed1bb3abd81f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 30 Sep 2019 15:58:33 +0200 Subject: [PATCH 26/80] add HgModifyCommand / implement delete method --- .../scm/repository/spi/HgModifyCommand.java | 72 +++++++++++++++++++ .../spi/HgRepositoryServiceProvider.java | 13 +++- .../spi/HgRepositoryServiceResolver.java | 10 +-- .../repository/spi/HgModifyCommandTest.java | 37 ++++++++++ 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java new file mode 100644 index 0000000000..df4c7f3420 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -0,0 +1,72 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.Changeset; +import com.aragost.javahg.Repository; +import com.aragost.javahg.commands.CommitCommand; +import com.aragost.javahg.commands.PushCommand; +import com.aragost.javahg.commands.RemoveCommand; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.util.WorkingCopy; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class HgModifyCommand implements ModifyCommand { + + private final HgRepositoryHandler handler; + private HgCommandContext context; + private final HgWorkdirFactory workdirFactory; + + public HgModifyCommand(HgRepositoryHandler handler, HgCommandContext context, HgWorkdirFactory workdirFactory) { + this.handler = handler; + this.context = context; + this.workdirFactory = workdirFactory; + } + + @Override + public String execute(ModifyCommandRequest request) { + + try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) { + Repository workingRepository = workingCopy.getWorkingRepository(); + request.getRequests().forEach( + partialRequest -> { + try { + partialRequest.execute(new Worker() { + @Override + public void delete(String toBeDeleted) { + RemoveCommand.on(workingRepository).execute(toBeDeleted); + } + + @Override + public void create(String toBeCreated, File file, boolean overwrite) { + + } + + @Override + public void modify(String path, File file) { + + } + + @Override + public void move(String sourcePath, String targetPath) { + + } + }); + } catch (IOException e) { + e.printStackTrace(); // TODO + } + } + ); + + CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute(); + List execute = PushCommand.on(workingRepository).execute(); + System.out.println(execute); + return execute.get(0).getNode(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index d60e888cac..79812a7f68 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -66,7 +66,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider Command.INCOMING, Command.OUTGOING, Command.PUSH, - Command.PULL + Command.PULL, + Command.MODIFY ); //J+ @@ -77,10 +78,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- HgRepositoryServiceProvider(HgRepositoryHandler handler, - HgHookManager hookManager, Repository repository) + HgHookManager hookManager, Repository repository, HgWorkdirFactory workdirFactory) { this.repository = repository; this.handler = handler; + this.workdirFactory = workdirFactory; this.repositoryDirectory = handler.getDirectory(repository.getId()); this.context = new HgCommandContext(hookManager, handler, repository, repositoryDirectory); @@ -238,6 +240,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider return new HgPushCommand(handler, context, repository); } + @Override + public ModifyCommand getModifyCommand() { + return new HgModifyCommand(handler, context, workdirFactory); + } + /** * Method description * @@ -287,4 +294,6 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider /** Field description */ private File repositoryDirectory; + + private final HgWorkdirFactory workdirFactory; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java index d6d04ee017..7b793e1b57 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java @@ -47,15 +47,17 @@ import sonia.scm.repository.Repository; public class HgRepositoryServiceResolver implements RepositoryServiceResolver { - private HgRepositoryHandler handler; - private HgHookManager hookManager; + private final HgRepositoryHandler handler; + private final HgHookManager hookManager; + private final HgWorkdirFactory workdirFactory; @Inject public HgRepositoryServiceResolver(HgRepositoryHandler handler, - HgHookManager hookManager) + HgHookManager hookManager, HgWorkdirFactory workdirFactory) { this.handler = handler; this.hookManager = hookManager; + this.workdirFactory = workdirFactory; } @Override @@ -63,7 +65,7 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver HgRepositoryServiceProvider provider = null; if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new HgRepositoryServiceProvider(handler, hookManager, repository); + provider = new HgRepositoryServiceProvider(handler, hookManager, repository, workdirFactory); } return provider; diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java new file mode 100644 index 0000000000..e612590ce2 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -0,0 +1,37 @@ +package sonia.scm.repository.spi; + +import com.google.inject.util.Providers; +import org.junit.Test; +import sonia.scm.repository.HgHookManager; +import sonia.scm.repository.Person; +import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.web.HgRepositoryEnvironmentBuilder; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class HgModifyCommandTest extends AbstractHgCommandTestBase { + + @Test + public void shouldRemoveFiles() throws IOException { + HgHookManager hookManager = mock(HgHookManager.class); + when(hookManager.getChallenge()).thenReturn("CHALLENGE"); + when(hookManager.getCredentials()).thenReturn("SECRET:SECRET"); + when(hookManager.createUrl()).thenReturn("http://localhost"); + HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); + + HgModifyCommand hgModifyCommand = new HgModifyCommand(handler, cmdContext, new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider())); + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); + request.setCommitMessage("this is great"); + request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com")); + + String result = hgModifyCommand.execute(request); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(result); + cmdContext.close(); + } +} From d406efb7bb3d99f2f28addf41260b9669ae6ce79 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 30 Sep 2019 16:45:56 +0200 Subject: [PATCH 27/80] add collapse all diffs by once button, implement level component --- .../ui-components/src/layout/Level.js | 19 +++++ .../ui-components/src/layout/index.js | 1 + .../packages/ui-components/src/repos/Diff.js | 5 +- .../ui-components/src/repos/DiffFile.js | 12 +++- .../ui-components/src/repos/LoadingDiff.js | 3 +- .../src/repos/changesets/ChangesetDiff.js | 5 +- scm-ui/public/locales/de/repos.json | 3 +- scm-ui/public/locales/en/repos.json | 3 +- scm-ui/public/locales/es/repos.json | 3 +- .../components/changesets/ChangesetDetails.js | 71 +++++++++++++------ 10 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 scm-ui-components/packages/ui-components/src/layout/Level.js diff --git a/scm-ui-components/packages/ui-components/src/layout/Level.js b/scm-ui-components/packages/ui-components/src/layout/Level.js new file mode 100644 index 0000000000..dd3e676c29 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/layout/Level.js @@ -0,0 +1,19 @@ +//@flow +import * as React from "react"; + +type Props = { + left?: React.Node, + right?: React.Node +}; + +export default class Level extends React.Component { + render() { + const { left, right } = this.props; + return ( +
+
{left}
+
{right}
+
+ ); + } +} diff --git a/scm-ui-components/packages/ui-components/src/layout/index.js b/scm-ui-components/packages/ui-components/src/layout/index.js index 7708c45c99..ac7279a06a 100644 --- a/scm-ui-components/packages/ui-components/src/layout/index.js +++ b/scm-ui-components/packages/ui-components/src/layout/index.js @@ -2,6 +2,7 @@ export { default as Footer } from "./Footer.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 PageActions } from "./PageActions.js"; export { default as Subtitle } from "./Subtitle.js"; diff --git a/scm-ui-components/packages/ui-components/src/repos/Diff.js b/scm-ui-components/packages/ui-components/src/repos/Diff.js index 7a5bcbf4a2..99a0e1d917 100644 --- a/scm-ui-components/packages/ui-components/src/repos/Diff.js +++ b/scm-ui-components/packages/ui-components/src/repos/Diff.js @@ -4,7 +4,8 @@ import DiffFile from "./DiffFile"; import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { - diff: File[] + diff: File[], + defaultCollapse?: boolean }; class Diff extends React.Component { @@ -17,7 +18,7 @@ class Diff extends React.Component { return ( <> {diff.map((file, index) => ( - + ))} ); diff --git a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js index 8dcdf0a640..f268940709 100644 --- a/scm-ui-components/packages/ui-components/src/repos/DiffFile.js +++ b/scm-ui-components/packages/ui-components/src/repos/DiffFile.js @@ -95,9 +95,19 @@ class DiffFile extends React.Component { }; } + // collapse diff by clicking collapseDiffs button + componentDidUpdate(prevProps) { + const { defaultCollapse } = this.props; + if (prevProps.defaultCollapse !== defaultCollapse) { + this.setState({ + collapsed: defaultCollapse + }); + } + } + toggleCollapse = () => { const { file } = this.props; - if(file && !file.isBinaray) { + if (file && !file.isBinaray) { this.setState(state => ({ collapsed: !state.collapsed })); diff --git a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js index 876b757f44..16f2d8e5f1 100644 --- a/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/LoadingDiff.js @@ -9,7 +9,8 @@ import Diff from "./Diff"; import type {DiffObjectProps, File} from "./DiffTypes"; type Props = DiffObjectProps & { - url: string + url: string, + defaultCollapse?: boolean }; type State = { diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js index d5d3c4e665..5bc6b249dd 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetDiff.js @@ -7,6 +7,7 @@ import {translate} from "react-i18next"; type Props = { changeset: Changeset, + defaultCollapse?: boolean, // context props t: string => string @@ -23,12 +24,12 @@ class ChangesetDiff extends React.Component { } render() { - const { changeset, t } = this.props; + const { changeset, defaultCollapse, t } = this.props; if (!this.isDiffSupported(changeset)) { return {t("changeset.diffNotSupported")}; } else { const url = this.createUrl(changeset); - return ; + return ; } } diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index 20cb7b4418..cec34319ab 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -73,7 +73,8 @@ "errorTitle": "Fehler", "errorSubtitle": "Changesets konnten nicht abgerufen werden", "noChangesets": "Keine Changesets in diesem Branch gefunden.", - "branchSelectorLabel": "Branches" + "branchSelectorLabel": "Branches", + "collapseDiffs": "Auf-/Zuklappen" }, "changeset": { "description": "Beschreibung", diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 29c0473ad9..76d21aa679 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -73,7 +73,8 @@ "errorTitle": "Error", "errorSubtitle": "Could not fetch changesets", "noChangesets": "No changesets found for this branch.", - "branchSelectorLabel": "Branches" + "branchSelectorLabel": "Branches", + "collapseDiffs": "Collapse" }, "changeset": { "description": "Description", diff --git a/scm-ui/public/locales/es/repos.json b/scm-ui/public/locales/es/repos.json index 08c9235593..efcbdd8c2e 100644 --- a/scm-ui/public/locales/es/repos.json +++ b/scm-ui/public/locales/es/repos.json @@ -73,7 +73,8 @@ "errorTitle": "Error", "errorSubtitle": "No se han podido recuperar los changesets", "noChangesets": "No se han encontrado changesets para esta rama branch.", - "branchSelectorLabel": "Ramas" + "branchSelectorLabel": "Ramas", + "collapseDiffs": "Colapso" }, "changeset": { "description": "Descripción", diff --git a/scm-ui/src/repos/components/changesets/ChangesetDetails.js b/scm-ui/src/repos/components/changesets/ChangesetDetails.js index 33d63a30d4..dde75ad98f 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetDetails.js +++ b/scm-ui/src/repos/components/changesets/ChangesetDetails.js @@ -1,9 +1,10 @@ //@flow import React from "react"; -import type { Changeset, Repository } from "@scm-manager/ui-types"; import { Interpolate, translate } from "react-i18next"; 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 { DateFromNow, ChangesetId, @@ -12,12 +13,23 @@ import { ChangesetDiff, AvatarWrapper, AvatarImage, - changesets + changesets, + Level, + Button } from "@scm-manager/ui-components"; -import classNames from "classnames"; -import type { Tag } from "@scm-manager/ui-types"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; +type Props = { + changeset: Changeset, + repository: Repository, + + // context props + t: string => string, + classes: any +}; + +type State = { + collapsed: boolean +}; const styles = { spacing: { @@ -30,16 +42,17 @@ const styles = { } }; -type Props = { - changeset: Changeset, - repository: Repository, - t: string => string, - classes: any -}; +class ChangesetDetails extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + collapsed: false + }; + } -class ChangesetDetails extends React.Component { render() { - const { changeset, repository, classes } = this.props; + const { changeset, repository, classes, t } = this.props; + const { collapsed } = this.state; const description = changesets.parseDescription(changeset.description); @@ -49,7 +62,7 @@ class ChangesetDetails extends React.Component { const date = ; return ( -
+ <>

{

- +

{this.renderTags()}
-

{description.message.split("\n").map((item, key) => { return ( @@ -99,9 +107,20 @@ class ChangesetDetails extends React.Component {

- + + } + /> +
- + ); } @@ -124,6 +143,12 @@ class ChangesetDetails extends React.Component { } return null; }; + + collapseDiffs = () => { + this.setState(state => ({ + collapsed: !state.collapsed + })); + }; } export default injectSheet(styles)(translate("repos")(ChangesetDetails)); From 4c8d28b9bff648ba12d7c97b7fe54f646ba27906 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 30 Sep 2019 16:47:06 +0200 Subject: [PATCH 28/80] correct trans file used --- scm-ui/src/repos/containers/ChangesetView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/repos/containers/ChangesetView.js b/scm-ui/src/repos/containers/ChangesetView.js index dc53b5d798..5a5af764c8 100644 --- a/scm-ui/src/repos/containers/ChangesetView.js +++ b/scm-ui/src/repos/containers/ChangesetView.js @@ -71,5 +71,5 @@ export default withRouter( connect( mapStateToProps, mapDispatchToProps - )(translate("changesets")(ChangesetView)) + )(translate("repos")(ChangesetView)) ); From c518236b57139c8e6254cc0d43c243d9056075ca Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Mon, 30 Sep 2019 17:01:40 +0200 Subject: [PATCH 29/80] remove some large doubled margin --- scm-ui/src/repos/components/changesets/ChangesetDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/repos/components/changesets/ChangesetDetails.js b/scm-ui/src/repos/components/changesets/ChangesetDetails.js index dde75ad98f..7f3553613d 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetDetails.js +++ b/scm-ui/src/repos/components/changesets/ChangesetDetails.js @@ -63,7 +63,7 @@ class ChangesetDetails extends React.Component { return ( <> -
+

Date: Tue, 1 Oct 2019 09:33:48 +0200 Subject: [PATCH 30/80] Error handling for plugin installation/uninstallaion --- .../components/MultiPluginActionModal.js | 17 ++++++++++++++++- .../src/admin/plugins/components/PluginModal.js | 1 + .../sonia/scm/plugin/DefaultPluginManager.java | 2 +- .../scm/plugin/PendingPluginInstallation.java | 2 +- .../scm/plugin/PendingPluginUninstallation.java | 2 +- ...uginFailedToCancelInstallationException.java | 15 ++++++++++++--- .../src/main/resources/locales/de/plugins.json | 4 ++++ .../src/main/resources/locales/en/plugins.json | 4 ++++ 8 files changed, 40 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js b/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js index a2a127d33f..394b7e48c9 100644 --- a/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js +++ b/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js @@ -97,7 +97,16 @@ class MultiPluginActionModal extends React.Component { loading: true }); - apiClient.post(pendingPlugins._links.cancel.href).then(() => this.reload()); + apiClient + .post(pendingPlugins._links.cancel.href) + .then(() => this.reload()) + .catch(error => { + this.setState({ + success: false, + loading: false, + error: error + }); + }); }; updateAll = () => { @@ -239,6 +248,7 @@ class MultiPluginActionModal extends React.Component { renderBody = () => { const { actionType } = this.props; + const { error } = this.state; return ( <>
@@ -247,6 +257,11 @@ class MultiPluginActionModal extends React.Component { {this.renderModalContent()}

+ {!!error && ( +
+ +
+ )} {actionType === MultiPluginActionType.EXECUTE_PENDING && (
{this.renderNotifications()}
)} diff --git a/scm-ui/src/admin/plugins/components/PluginModal.js b/scm-ui/src/admin/plugins/components/PluginModal.js index b15a068fa2..85e0f078d6 100644 --- a/scm-ui/src/admin/plugins/components/PluginModal.js +++ b/scm-ui/src/admin/plugins/components/PluginModal.js @@ -121,6 +121,7 @@ class PluginModal extends React.Component { .catch(error => { this.setState({ loading: false, + success: false, error: error }); }); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 4b4db3a162..07accbcc76 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -288,7 +288,7 @@ public class DefaultPluginManager implements PluginManager { pendingInstallQueue.forEach(PendingPluginInstallation::cancel); pendingUninstallQueue.clear(); pendingInstallQueue.clear(); - getInstalled().forEach(p -> p.setUninstallable(isUninstallable(p))); + updateMayUninstallFlag(); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java index 2f5d388db2..f2a8a4d633 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginInstallation.java @@ -30,7 +30,7 @@ class PendingPluginInstallation { Files.delete(file); plugin.cancelInstallation(); } catch (IOException ex) { - throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex); + throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java index 8d5a44d60a..7cbbecd151 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PendingPluginUninstallation.java @@ -26,7 +26,7 @@ class PendingPluginUninstallation { Files.delete(uninstallFile); plugin.setMarkedForUninstall(false); } catch (IOException ex) { - throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation of plugin " + name, ex); + throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation", name, ex); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java index e3d6c123d6..1be5ecdcf3 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginFailedToCancelInstallationException.java @@ -1,7 +1,16 @@ package sonia.scm.plugin; -public class PluginFailedToCancelInstallationException extends RuntimeException { - public PluginFailedToCancelInstallationException(String message, Throwable cause) { - super(message, cause); +import sonia.scm.ExceptionWithContext; + +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"; } } diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 9c5bb3f5f5..f7ba2e9adf 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -175,6 +175,10 @@ "40RaYIeeR1": { "displayName": "Es wurden keine Änderungen durchgeführt", "description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt 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." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 1ffffd73b7..a12ed3a194 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -175,6 +175,10 @@ "40RaYIeeR1": { "displayName": "No changes were made", "description": "No changes were made to the files of the repository. Therefor no new commit could be created." + }, + "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." } }, "namespaceStrategies": { From fb6d66978ecb58c00dcb8d44a8e0e56ed5a148c5 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 1 Oct 2019 09:36:27 +0200 Subject: [PATCH 31/80] add margin bottom for collapse button, add className option for level component --- .../packages/ui-components/src/layout/Level.js | 6 ++++-- scm-ui/src/repos/components/changesets/ChangesetDetails.js | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/layout/Level.js b/scm-ui-components/packages/ui-components/src/layout/Level.js index dd3e676c29..5359de7c66 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Level.js +++ b/scm-ui-components/packages/ui-components/src/layout/Level.js @@ -1,16 +1,18 @@ //@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 { render() { - const { left, right } = this.props; + const { className, left, right } = this.props; return ( -
+
{left}
{right}
diff --git a/scm-ui/src/repos/components/changesets/ChangesetDetails.js b/scm-ui/src/repos/components/changesets/ChangesetDetails.js index 7f3553613d..bea47925f5 100644 --- a/scm-ui/src/repos/components/changesets/ChangesetDetails.js +++ b/scm-ui/src/repos/components/changesets/ChangesetDetails.js @@ -35,6 +35,9 @@ const styles = { spacing: { marginRight: "1em" }, + bottomMargin: { + marginBottom: "1rem !important" + }, tags: { "& .tag": { marginLeft: ".25rem" @@ -108,6 +111,7 @@ class ChangesetDetails extends React.Component {
Date: Tue, 1 Oct 2019 09:41:56 +0200 Subject: [PATCH 32/80] Enhance error message --- scm-webapp/src/main/resources/locales/de/plugins.json | 2 +- scm-webapp/src/main/resources/locales/en/plugins.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index f7ba2e9adf..b687a7f8ca 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -178,7 +178,7 @@ }, "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." + "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": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index a12ed3a194..2e21ffe741 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -178,7 +178,7 @@ }, "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." + "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": { From 92af956c5e476605250372e80b243df6fe85491d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 1 Oct 2019 09:55:00 +0200 Subject: [PATCH 33/80] Add unique keys for react --- scm-ui/src/admin/plugins/containers/PluginsOverview.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js index 92a5e0f231..f13ce852cd 100644 --- a/scm-ui/src/admin/plugins/containers/PluginsOverview.js +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -122,6 +122,7 @@ class PluginsOverview extends React.Component { ) { buttons.push( @@ -135,6 +136,7 @@ class PluginsOverview extends React.Component { ) { buttons.push( @@ -144,6 +146,7 @@ class PluginsOverview extends React.Component { if (collection && collection._links && collection._links.update) { buttons.push( From e6d32a14681d6dc6ab1265bdd73571080320d597 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 1 Oct 2019 10:27:06 +0200 Subject: [PATCH 34/80] Replace hard reload with refresh --- .../admin/plugins/components/MultiPluginAction.js | 4 +++- .../plugins/components/MultiPluginActionModal.js | 12 +++++++----- .../src/admin/plugins/containers/PluginsOverview.js | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/scm-ui/src/admin/plugins/components/MultiPluginAction.js b/scm-ui/src/admin/plugins/components/MultiPluginAction.js index 2fe3ae6985..7f37a0d7cf 100644 --- a/scm-ui/src/admin/plugins/components/MultiPluginAction.js +++ b/scm-ui/src/admin/plugins/components/MultiPluginAction.js @@ -15,6 +15,7 @@ type Props = { actionType: string, pendingPlugins?: PendingPlugins, installedPlugins?: PluginCollection, + refresh: () => void, // context props t: (key: string, params?: Object) => string @@ -57,7 +58,7 @@ class MultiPluginAction extends React.Component { renderModal = () => { const { showModal } = this.state; - const { pendingPlugins, installedPlugins, actionType } = this.props; + const { pendingPlugins, installedPlugins, actionType, refresh } = this.props; if (showModal) { return ( { : installedPlugins } onClose={this.toggleModal} + refresh={refresh} actionType={actionType} /> ); diff --git a/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js b/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js index 394b7e48c9..b9c9aa9b8e 100644 --- a/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js +++ b/scm-ui/src/admin/plugins/components/MultiPluginActionModal.js @@ -19,6 +19,7 @@ type Props = { actionType: string, pendingPlugins?: PendingPlugins, installedPlugins?: PluginCollection, + refresh: () => void, // context props t: string => string @@ -99,7 +100,7 @@ class MultiPluginActionModal extends React.Component { apiClient .post(pendingPlugins._links.cancel.href) - .then(() => this.reload()) + .then(() => this.refresh()) .catch(error => { this.setState({ success: false, @@ -117,18 +118,19 @@ class MultiPluginActionModal extends React.Component { apiClient .post(installedPlugins._links.update.href) - .then(() => this.reload()); + .then(() => this.refresh()); }; - reload = () => { - window.location.reload(true); + refresh = () => { + this.props.refresh(); + this.props.onClose(); }; renderModalContent = () => { const { actionType } = this.props; if (actionType === MultiPluginActionType.UPDATE_ALL) { - return <>{this.renderUpdatable()}; + return this.renderUpdatable(); } else { return ( <> diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js index f13ce852cd..e5cd4aca7b 100644 --- a/scm-ui/src/admin/plugins/containers/PluginsOverview.js +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -124,6 +124,7 @@ class PluginsOverview extends React.Component { ); @@ -138,6 +139,7 @@ class PluginsOverview extends React.Component { ); @@ -148,6 +150,7 @@ class PluginsOverview extends React.Component { ); From e0c40c645a2c798dd1c189ef63f6c7d844e11151 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 1 Oct 2019 10:31:55 +0200 Subject: [PATCH 35/80] Replace p with div Buttons can have divs for themselves, which will lead to an error otherwise because a p must not contain divs. --- .../packages/ui-components/src/buttons/ButtonGroup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js b/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js index b73688ebbf..66b3f21052 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js +++ b/scm-ui-components/packages/ui-components/src/buttons/ButtonGroup.js @@ -14,7 +14,7 @@ class ButtonGroup extends React.Component { const childWrapper = []; React.Children.forEach(children, child => { if (child) { - childWrapper.push(

{child}

); + childWrapper.push(
{child}
); } }); From 7f657e53605545546c60a72c948fbe59e2172eac Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 1 Oct 2019 13:21:08 +0200 Subject: [PATCH 36/80] Simplifications --- .../plugins/components/MultiPluginAction.js | 25 ++++++++----------- .../components/MultiPluginActionModal.js | 21 ++++++---------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/scm-ui/src/admin/plugins/components/MultiPluginAction.js b/scm-ui/src/admin/plugins/components/MultiPluginAction.js index 7f37a0d7cf..cf20dbbca8 100644 --- a/scm-ui/src/admin/plugins/components/MultiPluginAction.js +++ b/scm-ui/src/admin/plugins/components/MultiPluginAction.js @@ -41,15 +41,15 @@ class MultiPluginAction extends React.Component { renderLabel = () => { const { t, actionType, installedPlugins } = this.props; - const outdatedPlugins = - actionType === MultiPluginActionType.UPDATE_ALL && - installedPlugins._embedded.plugins.filter(p => p._links.update).length; if (actionType === MultiPluginActionType.EXECUTE_PENDING) { return t("plugins.executePending"); } else if (actionType === MultiPluginActionType.CANCEL_PENDING) { return t("plugins.cancelPending"); } else { + const outdatedPlugins = installedPlugins._embedded.plugins.filter( + p => p._links.update + ).length; return t("plugins.outdatedPlugins", { count: outdatedPlugins }); @@ -58,20 +58,17 @@ class MultiPluginAction extends React.Component { renderModal = () => { const { showModal } = this.state; - const { pendingPlugins, installedPlugins, actionType, refresh } = this.props; + const { + pendingPlugins, + installedPlugins, + actionType, + refresh + } = this.props; if (showModal) { return ( { }; renderModalContent = () => { - const { actionType } = this.props; - - if (actionType === MultiPluginActionType.UPDATE_ALL) { - return this.renderUpdatable(); - } else { - return ( - <> - {this.renderInstallQueue()} - {this.renderUpdateQueue()} - {this.renderUninstallQueue()} - - ); - } + return ( + <> + {this.renderUpdatable()} + {this.renderInstallQueue()} + {this.renderUpdateQueue()} + {this.renderUninstallQueue()} + + ); }; renderUpdatable = () => { From 6605cc2b6b308adce24845fa8fcb17e911eba97a Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 1 Oct 2019 13:58:33 +0200 Subject: [PATCH 37/80] remove unused imports --- .../src/main/java/sonia/scm/repository/spi/GitCatCommand.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index c9f33d84f4..35ff4d6ac2 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -32,8 +32,6 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.attributes.Attribute; -import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; @@ -45,7 +43,6 @@ import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; -import org.eclipse.jgit.util.LfsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; From 35b8740e00f436d5547e191abad2c93f0feaba7a Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 1 Oct 2019 12:12:04 +0000 Subject: [PATCH 38/80] Close branch feature/cat_with_lfs From fb5617d94048ce26439972507c4e03fd8c680801 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 1 Oct 2019 14:41:56 +0200 Subject: [PATCH 39/80] implement create method for HgModifyCommand --- .../scm/repository/spi/HgModifyCommand.java | 53 ++++++++++- .../repository/spi/HgModifyCommandTest.java | 91 ++++++++++++++++++- 2 files changed, 138 insertions(+), 6 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java index df4c7f3420..de39c233b4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -5,13 +5,23 @@ import com.aragost.javahg.Repository; import com.aragost.javahg.commands.CommitCommand; import com.aragost.javahg.commands.PushCommand; import com.aragost.javahg.commands.RemoveCommand; +import org.apache.commons.lang.StringUtils; +import sonia.scm.ContextEntry; import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.util.WorkingCopy; import java.io.File; import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static sonia.scm.AlreadyExistsException.alreadyExists; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + public class HgModifyCommand implements ModifyCommand { private final HgRepositoryHandler handler; @@ -39,8 +49,22 @@ public class HgModifyCommand implements ModifyCommand { } @Override - public void create(String toBeCreated, File file, boolean overwrite) { - + public void create(String toBeCreated, File file, boolean overwrite) throws IOException { + Path targetFile = new File(workingRepository.getDirectory(), toBeCreated).toPath(); + if (overwrite) { + Files.move(file.toPath(), targetFile, REPLACE_EXISTING); + } else { + try { + Files.move(file.toPath(), targetFile); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(createFileContext(toBeCreated)); + } + } + try { + addFileToHg(targetFile.toFile()); + } catch (Exception e) { + throwInternalRepositoryException("could not add new file to index", e); + } } @Override @@ -52,6 +76,28 @@ public class HgModifyCommand implements ModifyCommand { public void move(String sourcePath, String targetPath) { } + + private void createDirectories(Path targetFile) throws IOException { + try { + Files.createDirectories(targetFile.getParent()); + } catch (FileAlreadyExistsException e) { + throw alreadyExists(createFileContext(targetFile.toString())); + } + } + + private ContextEntry.ContextBuilder createFileContext(String path) { + ContextEntry.ContextBuilder contextBuilder = entity("file", path); + if (!StringUtils.isEmpty(request.getBranch())) { + contextBuilder.in("branch", request.getBranch()); + } + contextBuilder.in(context.getScmRepository()); + return contextBuilder; + } + + private void addFileToHg(File file) { + workingRepository.workingCopy().add(file.getAbsolutePath()); + } + }); } catch (IOException e) { e.printStackTrace(); // TODO @@ -67,6 +113,9 @@ public class HgModifyCommand implements ModifyCommand { e.printStackTrace(); return null; } + } + private String throwInternalRepositoryException(String message, Exception e) { + throw new InternalRepositoryException(context.getScmRepository(), message, e); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java index e612590ce2..d808b9e454 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -1,12 +1,19 @@ package sonia.scm.repository.spi; import com.google.inject.util.Providers; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.AlreadyExistsException; import sonia.scm.repository.HgHookManager; import sonia.scm.repository.Person; import sonia.scm.repository.util.WorkdirProvider; import sonia.scm.web.HgRepositoryEnvironmentBuilder; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; @@ -15,15 +22,28 @@ import static org.mockito.Mockito.when; public class HgModifyCommandTest extends AbstractHgCommandTestBase { - @Test - public void shouldRemoveFiles() throws IOException { + private HgModifyCommand hgModifyCommand; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void initHgModifyCommand() { HgHookManager hookManager = mock(HgHookManager.class); when(hookManager.getChallenge()).thenReturn("CHALLENGE"); when(hookManager.getCredentials()).thenReturn("SECRET:SECRET"); when(hookManager.createUrl()).thenReturn("http://localhost"); HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager); + hgModifyCommand = new HgModifyCommand(handler, cmdContext, new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider())); + } - HgModifyCommand hgModifyCommand = new HgModifyCommand(handler, cmdContext, new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider())); + @After + public void closeRepository() throws IOException { + cmdContext.close(); + } + + @Test + public void shouldRemoveFiles() { ModifyCommandRequest request = new ModifyCommandRequest(); request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt")); request.setCommitMessage("this is great"); @@ -32,6 +52,69 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase { String result = hgModifyCommand.execute(request); assertThat(cmdContext.open().tip().getNode()).isEqualTo(result); - cmdContext.close(); + } + + @Test + public void shouldCreateFilesWithoutOverwrite() throws IOException { + + File testFile = temporaryFolder.newFile("Answer.txt"); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet = hgModifyCommand.execute(request); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet); + assertThat(cmdContext.open().tip().getAddedFiles().size()).isEqualTo(1); + } + + @Test + public void shouldOverwriteExistingFiles() throws IOException { + + File testFile = temporaryFolder.newFile("Answer.txt"); + new FileOutputStream(testFile).write(21); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, true)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + String changeSet2 = hgModifyCommand.execute(request2); + + assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2); + assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1); + assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo(testFile.getName()); + } + + @Test(expected = AlreadyExistsException.class) + public void shouldThrowFileAlreadyExistsException() throws IOException { + + File testFile = temporaryFolder.newFile("Answer.txt"); + new FileOutputStream(testFile).write(21); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request.setCommitMessage("I found the answer"); + request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request); + + new FileOutputStream(testFile).write(42); + ModifyCommandRequest request2 = new ModifyCommandRequest(); + request2.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false)); + request2.setCommitMessage(" Now i really found the answer"); + request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com")); + + hgModifyCommand.execute(request2); } } From c9966081d49613aa94771facf88ed339023734ee Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 1 Oct 2019 12:52:25 +0000 Subject: [PATCH 40/80] Close branch bugfix/postpone_writing_to_stream From 3abf9815f32f211080a37cf0b0ca2913b5df5f64 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 1 Oct 2019 17:03:27 +0200 Subject: [PATCH 41/80] Externalize modal dialogs --- .../components/CancelPendingActionModal.js | 39 ++++++ .../components/ExecutePendingActionModal.js | 41 +++++++ .../plugins/components/MultiPluginAction.js | 46 +------ .../components/MultiPluginActionModal.js | 113 +++--------------- .../components/UpdateAllActionModal.js | 39 ++++++ .../plugins/containers/PluginsOverview.js | 50 +++++++- 6 files changed, 185 insertions(+), 143 deletions(-) create mode 100644 scm-ui/src/admin/plugins/components/CancelPendingActionModal.js create mode 100644 scm-ui/src/admin/plugins/components/ExecutePendingActionModal.js create mode 100644 scm-ui/src/admin/plugins/components/UpdateAllActionModal.js diff --git a/scm-ui/src/admin/plugins/components/CancelPendingActionModal.js b/scm-ui/src/admin/plugins/components/CancelPendingActionModal.js new file mode 100644 index 0000000000..f18c7d81f4 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/CancelPendingActionModal.js @@ -0,0 +1,39 @@ +// @flow + +import React from "react"; +import MultiPluginActionModal from "./MultiPluginActionModal"; +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 { + + render() { + const {onClose, pendingPlugins, t} = this.props; + + return + ; + } + + cancelPending = () => { + const { pendingPlugins, refresh, onClose } = this.props; + return apiClient + .post(pendingPlugins._links.cancel.href) + .then(refresh) + .then(onClose); + }; +} + +export default translate("admin")(CancelPendingActionModal); diff --git a/scm-ui/src/admin/plugins/components/ExecutePendingActionModal.js b/scm-ui/src/admin/plugins/components/ExecutePendingActionModal.js new file mode 100644 index 0000000000..747497cefb --- /dev/null +++ b/scm-ui/src/admin/plugins/components/ExecutePendingActionModal.js @@ -0,0 +1,41 @@ +// @flow + +import React from "react"; +import MultiPluginActionModal from "./MultiPluginActionModal"; +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 { + + render() { + const {onClose, pendingPlugins, t} = this.props; + + return + + {t("plugins.modal.restartNotification")} + + ; + } + + executeAndRestart = () => { + const {pendingPlugins} = this.props; + return apiClient + .post(pendingPlugins._links.execute.href) + .then(waitForRestart); + }; +} + +export default translate("admin")(ExecutePendingActionModal); diff --git a/scm-ui/src/admin/plugins/components/MultiPluginAction.js b/scm-ui/src/admin/plugins/components/MultiPluginAction.js index cf20dbbca8..05e145f215 100644 --- a/scm-ui/src/admin/plugins/components/MultiPluginAction.js +++ b/scm-ui/src/admin/plugins/components/MultiPluginAction.js @@ -3,7 +3,6 @@ import React from "react"; import { Button } from "@scm-manager/ui-components"; import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; -import MultiPluginActionModal from "./MultiPluginActionModal"; export const MultiPluginActionType = { UPDATE_ALL: "updateAll", @@ -16,28 +15,13 @@ type Props = { pendingPlugins?: PendingPlugins, installedPlugins?: PluginCollection, refresh: () => void, + onClick: () => void, // context props t: (key: string, params?: Object) => string }; -type State = { - showModal: boolean -}; - -class MultiPluginAction extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - showModal: false - }; - } - - toggleModal = () => { - this.setState(state => ({ - showModal: !state.showModal - })); - }; +class MultiPluginAction extends React.Component { renderLabel = () => { const { t, actionType, installedPlugins } = this.props; @@ -56,28 +40,6 @@ class MultiPluginAction extends React.Component { } }; - renderModal = () => { - const { showModal } = this.state; - const { - pendingPlugins, - installedPlugins, - actionType, - refresh - } = this.props; - if (showModal) { - return ( - - ); - } - return null; - }; - renderIcon = () => { const { actionType } = this.props; @@ -91,15 +53,15 @@ class MultiPluginAction extends React.Component { }; render() { + const { onClick } = this.props; return ( <> - {this.renderModal()}