diff --git a/scm-core/src/main/java/sonia/scm/repository/NoCommonHistoryException.java b/scm-core/src/main/java/sonia/scm/repository/NoCommonHistoryException.java new file mode 100644 index 0000000000..e63f28cdce --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/NoCommonHistoryException.java @@ -0,0 +1,18 @@ +package sonia.scm.repository; + +import sonia.scm.BadRequestException; + +import static java.util.Collections.emptyList; + +@SuppressWarnings("squid:MaximumInheritanceDepth") +public class NoCommonHistoryException extends BadRequestException { + + public NoCommonHistoryException() { + super(emptyList(), "no common history"); + } + + @Override + public String getCode() { + return "4iRct4avG1"; + } +} 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 computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { + public static ObjectId 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)); - RevCommit commonAncestor = mergeBaseWalk.next(); - return ofNullable(commonAncestor).map(RevCommit::getId); + RevCommit ancestor = mergeBaseWalk.next(); + if (ancestor == null) { + throw new NoCommonHistoryException(); + } + return ancestor.getId(); + } + } + + 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(); } } 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 81d19886c3..0204ca4e3c 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,15 +10,11 @@ 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 { @@ -59,9 +55,7 @@ final class Differ implements AutoCloseable { if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) { ObjectId otherRevision = repository.resolve(request.getAncestorChangeset()); - ObjectId ancestorId = - computeCommonAncestor(repository, revision, otherRevision) - .orElseThrow(NoCommonHistoryException::new); + ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision); RevTree tree = walk.parseCommit(ancestorId).getTree(); treeWalk.addTree(tree); } @@ -88,10 +82,6 @@ final class Differ implements AutoCloseable { return new Differ(commit, walk, treeWalk); } - private static Optional computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException { - return GitUtil.computeCommonAncestor(repository, revision1, revision2); - } - private Diff diff() throws IOException { List entries = DiffEntry.scan(treeWalk); return new Diff(commit, entries); @@ -122,15 +112,4 @@ final class Differ implements AutoCloseable { } } - private static class NoCommonHistoryException extends BadRequestException { - - private NoCommonHistoryException() { - super(emptyList(), "no common history"); - } - - @Override - public String getCode() { - return "4iRct4avG1"; - } - } } 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..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,13 +58,17 @@ 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; import java.io.ByteArrayOutputStream; 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; @@ -86,18 +91,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 ---------------------------------------------------------- @@ -167,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(); @@ -195,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()) @@ -203,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)); @@ -232,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; @@ -339,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; @@ -364,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()) { @@ -375,7 +391,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()); } @@ -389,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); @@ -410,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 7477e0aee3..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 @@ -33,6 +33,7 @@ package sonia.scm.repository.spi; 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; @@ -45,13 +46,18 @@ import org.eclipse.jgit.treewalk.filter.PathFilter; 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; import java.io.Closeable; 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; @@ -61,15 +67,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 +89,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { } void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException { - 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 +125,67 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { treeWalk.setFilter(PathFilter.create(path)); if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { - ObjectId blobId = treeWalk.getObjectId(0); - ObjectLoader loader = repo.open(blobId); - - return new ClosableObjectLoaderContainer(loader, 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); + } } else { throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository)); } } - private static class ClosableObjectLoaderContainer implements Closeable { + private Loader loadFromGit(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + ObjectLoader loader = repo.open(blobId); + + return new GitObjectLoaderWrapper(loader, treeWalk, revWalk); + } + + private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException { + BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); + Blob blob = lfsBlobStore.get(lfsPointer.getOid().getName()); + GitUtil.release(revWalk); + GitUtil.release(treeWalk); + return new BlobLoader(blob); + } + + private interface Loader extends Closeable { + void copyTo(OutputStream output) throws IOException; + + InputStream openStream() throws IOException; + } + + private static class BlobLoader implements Loader { + private final InputStream inputStream; + + private BlobLoader(Blob blob) throws IOException { + this.inputStream = blob.getInputStream(); + } + + @Override + public void copyTo(OutputStream output) throws IOException { + IOUtil.copy(inputStream, output); + } + + @Override + public InputStream openStream() { + return inputStream; + } + + @Override + public void close() throws IOException { + this.inputStream.close(); + } + } + + private static class GitObjectLoaderWrapper implements Loader { private final ObjectLoader objectLoader; private final 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 +196,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/GitDiffCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java index 5d5f27806b..1ac64c1b5e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitDiffCommand.java @@ -31,15 +31,12 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; import sonia.scm.repository.Repository; +import sonia.scm.repository.api.DiffCommandBuilder; -import java.io.BufferedOutputStream; import java.io.IOException; -import java.io.OutputStream; /** * @@ -52,22 +49,25 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { } @Override - public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException { + public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException { @SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService org.eclipse.jgit.lib.Repository repository = open(); - try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) { - formatter.setRepository(repository); - Differ.Diff diff = Differ.diff(repository, request); + Differ.Diff diff = Differ.diff(repository, request); - for (DiffEntry e : diff.getEntries()) { - if (!e.getOldId().equals(e.getNewId())) { - formatter.format(e); + return output -> { + 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/main/java/sonia/scm/repository/spi/GitHunkParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java index b7594bb5d6..a65b9a6b02 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitHunkParser.java @@ -79,7 +79,9 @@ final class GitHunkParser { ++oldLineCounter; break; default: - throw new IllegalStateException("cannot handle diff line: " + line); + if (!line.equals("\\ No newline at end of file")) { + throw new IllegalStateException("cannot handle diff line: " + line); + } } } 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/java/sonia/scm/repository/spi/GitDiffCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitDiffCommandTest.java index fd9c45be5c..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, output); + 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, output); + 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, output); + 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, output); + 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, output); + gitDiffCommand.getDiffResult(diffCommandRequest).accept(output); assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString()); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java index a58fae644d..e3f09ce5ef 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitHunkParserTest.java @@ -68,6 +68,17 @@ class GitHunkParserTest { " a\n" + "~illegal line\n"; + private static final String NO_NEWLINE_DIFF = "diff --git a/.editorconfig b/.editorconfig\n" + + "index ea2a3ba..2f02f32 100644\n" + + "--- a/.editorconfig\n" + + "+++ b/.editorconfig\n" + + "@@ -10,3 +10,4 @@\n" + + " indent_style = space\n" + + " indent_size = 2\n" + + " charset = utf-8\n" + + "+added line\n" + + "\\ No newline at end of file\n"; + @Test void shouldParseHunks() { List hunks = new GitHunkParser().parse(DIFF_001); @@ -127,6 +138,27 @@ class GitHunkParserTest { assertThrows(IllegalStateException.class, () -> new GitHunkParser().parse(ILLEGAL_DIFF)); } + @Test + void shouldIgnoreNoNewlineLine() { + List hunks = new GitHunkParser().parse(NO_NEWLINE_DIFF); + + Hunk hunk = hunks.get(0); + + Iterator lines = hunk.iterator(); + + DiffLine line1 = lines.next(); + assertThat(line1.getOldLineNumber()).hasValue(10); + assertThat(line1.getNewLineNumber()).hasValue(10); + assertThat(line1.getContent()).isEqualTo("indent_style = space"); + + lines.next(); + lines.next(); + DiffLine lastLine = lines.next(); + assertThat(lastLine.getOldLineNumber()).isEmpty(); + assertThat(lastLine.getNewLineNumber()).hasValue(13); + assertThat(lastLine.getContent()).isEqualTo("added line"); + } + private void assertHunk(Hunk hunk, int oldStart, int oldLineCount, int newStart, int newLineCount) { assertThat(hunk.getOldStart()).isEqualTo(oldStart); assertThat(hunk.getOldLineCount()).isEqualTo(oldLineCount); 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 8f689e9664..addda86090 100644 Binary files a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip and b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip differ 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..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 @@ -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,41 +70,36 @@ 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(); + return output -> { + com.aragost.javahg.Repository hgRepo = open(); - HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo); - DiffFormat format = request.getFormat(); + 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 - { - - if (!Strings.isNullOrEmpty(request.getPath())) + if (format == DiffFormat.GIT) { - inputStream = cmd.stream(hgRepo.file(request.getPath())); - } - else - { - inputStream = cmd.stream(); + cmd.git(); } - ByteStreams.copy(inputStream, output); + cmd.change(HgUtil.getRevision(request.getRevision())); - } - finally - { - Closeables.close(inputStream, true); - } + InputStream inputStream = null; + + try { + + 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); + } + }; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js index b0935b238e..2049850e8e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgConfigurationForm.js @@ -5,15 +5,15 @@ import { translate } from "react-i18next"; import { InputField, Checkbox } from "@scm-manager/ui-components"; type Configuration = { - "hgBinary": string, - "pythonBinary": string, - "pythonPath"?: string, - "encoding": string, - "useOptimizedBytecode": boolean, - "showRevisionInId": boolean, - "disableHookSSLValidation": boolean, - "enableHttpPostArgs": boolean, - "_links": Links + hgBinary: string, + pythonBinary: string, + pythonPath?: string, + encoding: string, + useOptimizedBytecode: boolean, + showRevisionInId: boolean, + disableHookSSLValidation: boolean, + enableHttpPostArgs: boolean, + _links: Links }; type Props = { @@ -23,29 +23,26 @@ type Props = { onConfigurationChange: (Configuration, boolean) => void, // context props - t: (string) => string -} + t: string => string +}; type State = Configuration & { validationErrors: string[] }; class HgConfigurationForm extends React.Component { - constructor(props: Props) { super(props); this.state = { ...props.initialConfiguration, validationErrors: [] }; } updateValidationStatus = () => { - const requiredFields = [ - "hgBinary", "pythonBinary", "encoding" - ]; + const requiredFields = ["hgBinary", "pythonBinary", "encoding"]; const validationErrors = []; for (let field of requiredFields) { if (!this.state[field]) { - validationErrors.push( field ); + validationErrors.push(field); } } @@ -56,58 +53,73 @@ class HgConfigurationForm extends React.Component { return validationErrors.length === 0; }; - hasValidationError = (name: string) => { return this.state.validationErrors.indexOf(name) >= 0; }; handleChange = (value: any, name: string) => { - this.setState({ - [name]: value - }, () => this.props.onConfigurationChange(this.state, this.updateValidationStatus())); + this.setState( + { + [name]: value + }, + () => + this.props.onConfigurationChange( + this.state, + this.updateValidationStatus() + ) + ); }; inputField = (name: string) => { const { readOnly, t } = this.props; - return ; + return ( +
+ +
+ ); }; checkbox = (name: string) => { const { readOnly, t } = this.props; - return ; + return ( + + ); }; render() { return ( - <> +
{this.inputField("hgBinary")} {this.inputField("pythonBinary")} {this.inputField("pythonPath")} {this.inputField("encoding")} - {this.checkbox("useOptimizedBytecode")} - {this.checkbox("showRevisionInId")} - {this.checkbox("disableHookSSLValidation")} - {this.checkbox("enableHttpPostArgs")} - +
+ {this.checkbox("useOptimizedBytecode")} + {this.checkbox("showRevisionInId")} +
+
+ {this.checkbox("disableHookSSLValidation")} + {this.checkbox("enableHttpPostArgs")} +
+
); } - } export default translate("plugins")(HgConfigurationForm); 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-ui-components/packages/ui-components/src/Breadcrumb.js b/scm-ui-components/packages/ui-components/src/Breadcrumb.js index 5b0f151c61..74996accc0 100644 --- a/scm-ui-components/packages/ui-components/src/Breadcrumb.js +++ b/scm-ui-components/packages/ui-components/src/Breadcrumb.js @@ -1,10 +1,12 @@ //@flow import React from "react"; -import {Link} from "react-router-dom"; -import type {Branch, Repository} from "@scm-manager/ui-types"; +import { Link } from "react-router-dom"; +import { translate } from "react-i18next"; import injectSheet from "react-jss"; -import {binder, ExtensionPoint} from "@scm-manager/ui-extensions"; import classNames from "classnames"; +import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import type { Branch, Repository } from "@scm-manager/ui-types"; +import Icon from "./Icon"; type Props = { repository: Repository, @@ -14,7 +16,10 @@ type Props = { revision: string, path: string, baseUrl: string, - classes: any + + // Context props + classes: any, + t: string => string }; const styles = { @@ -28,6 +33,9 @@ const styles = { flexStart: { flex: "1" }, + homeIcon: { + lineHeight: "1.5rem" + }, buttonGroup: { alignSelf: "center", paddingRight: "1rem" @@ -45,7 +53,7 @@ class Breadcrumb extends React.Component { if (paths.length - 1 === index) { return (
  • - + {path}
  • @@ -59,19 +67,20 @@ class Breadcrumb extends React.Component { }); return map; } - return
  • ; + return null; } render() { const { - classes, baseUrl, branch, defaultBranch, branches, revision, path, - repository + repository, + classes, + t } = this.props; return ( @@ -84,7 +93,19 @@ class Breadcrumb extends React.Component { )} aria-label="breadcrumbs" > -
      {this.renderPath()}
    +
      +
    • + + + +
    • + {this.renderPath()} +
    {binder.hasExtension("repos.sources.actionbar") && (
    @@ -112,4 +133,4 @@ class Breadcrumb extends React.Component { } } -export default injectSheet(styles)(Breadcrumb); +export default translate("commons")(injectSheet(styles)(Breadcrumb)); diff --git a/scm-ui-components/packages/ui-components/src/CardColumn.js b/scm-ui-components/packages/ui-components/src/CardColumn.js index 58722c0cac..5340d88a35 100644 --- a/scm-ui-components/packages/ui-components/src/CardColumn.js +++ b/scm-ui-components/packages/ui-components/src/CardColumn.js @@ -21,21 +21,22 @@ const styles = { flexFullHeight: { display: "flex", flexDirection: "column", + justifyContent: "space-around", alignSelf: "stretch" }, - content: { - display: "flex", - flexGrow: 1, - alignItems: "center", - justifyContent: "space-between" - }, footer: { display: "flex", - marginTop: "auto", - paddingBottom: "1.5rem" + paddingBottom: "1rem", }, - noBottomMargin: { - marginBottom: "0 !important" + topPart: { + display: "flex" + }, + contentRight: { + marginLeft: "auto" + }, + contentLeft: { + marginBottom: "0 !important", + overflow: "hidden" } }; @@ -91,25 +92,26 @@ class CardColumn extends React.Component { classes.flexFullHeight )} > -
    +
    -

    +

    {title}

    {description}

    +
    {contentRight && contentRight} +
    -
    -
    {footerLeft}
    -
    {footerRight}
    -
    +
    +
    {footerLeft}
    +
    {footerRight}
    +
    diff --git a/scm-ui-components/packages/ui-components/src/Help.js b/scm-ui-components/packages/ui-components/src/Help.js index 9cb37e722d..6b632810e9 100644 --- a/scm-ui-components/packages/ui-components/src/Help.js +++ b/scm-ui-components/packages/ui-components/src/Help.js @@ -1,8 +1,9 @@ //@flow import React from "react"; import injectSheet from "react-jss"; -import Tooltip from './Tooltip'; -import HelpIcon from './HelpIcon'; +import classNames from "classnames"; +import Tooltip from "./Tooltip"; +import HelpIcon from "./HelpIcon"; const styles = { tooltip: { @@ -14,21 +15,22 @@ const styles = { type Props = { message: string, + className?: string, classes: any -} +}; class Help extends React.Component { - render() { - const { message, classes } = this.props; + const { message, className, classes } = this.props; return ( - + ); } - } export default injectSheet(styles)(Help); - diff --git a/scm-ui-components/packages/ui-components/src/HelpIcon.js b/scm-ui-components/packages/ui-components/src/HelpIcon.js index 9e095bd8a7..c73417bb27 100644 --- a/scm-ui-components/packages/ui-components/src/HelpIcon.js +++ b/scm-ui-components/packages/ui-components/src/HelpIcon.js @@ -1,22 +1,24 @@ //@flow import React from "react"; import injectSheet from "react-jss"; -import classNames from "classnames"; +import Icon from "./Icon"; type Props = { classes: any }; const styles = { - textinfo: { - color: "#98d8f3 !important" - } + textinfo: { + color: "#98d8f3 !important" + } }; class HelpIcon extends React.Component { render() { const { classes } = this.props; - return ; + return ( + + ); } } diff --git a/scm-ui-components/packages/ui-components/src/Icon.js b/scm-ui-components/packages/ui-components/src/Icon.js index b3b9a9b4c2..255e3ef6cd 100644 --- a/scm-ui-components/packages/ui-components/src/Icon.js +++ b/scm-ui-components/packages/ui-components/src/Icon.js @@ -4,22 +4,26 @@ import classNames from "classnames"; type Props = { title?: string, - name: string -} + name: string, + color: string, + className?: string +}; export default class Icon extends React.Component { + static defaultProps = { + color: "grey-light" + }; render() { - const { title, name } = this.props; - if(title) { + const { title, name, color, className } = this.props; + if (title) { return ( - + ); } - return ( - - ); + return ; } - } - diff --git a/scm-ui-components/packages/ui-components/src/MarkdownView.js b/scm-ui-components/packages/ui-components/src/MarkdownView.js index 4620004f59..6176601d68 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownView.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownView.js @@ -1,11 +1,11 @@ //@flow import React from "react"; -import SyntaxHighlighter from "./SyntaxHighlighter"; -import Markdown from "react-markdown/with-html"; -import {binder} from "@scm-manager/ui-extensions"; -import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; import { withRouter } from "react-router-dom"; - +import injectSheet from "react-jss"; +import Markdown from "react-markdown/with-html"; +import { binder } from "@scm-manager/ui-extensions"; +import SyntaxHighlighter from "./SyntaxHighlighter"; +import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; type Props = { content: string, @@ -14,11 +14,34 @@ type Props = { enableAnchorHeadings: boolean, // context props + classes: any, location: any }; -class MarkdownView extends React.Component { +const styles = { + markdown: { + "& > .content": { + "& > h1, h2, h3, h4, h5, h6": { + margin: "0.5rem 0", + fontSize: "0.9rem" + }, + "& > h1": { + fontWeight: "700" + }, + "& > h2": { + fontWeight: "600" + }, + "& > h3, h4, h5, h6": { + fontWeight: "500" + }, + "& strong": { + fontWeight: "500" + } + } + } +}; +class MarkdownView extends React.Component { static defaultProps = { enableAnchorHeadings: false }; @@ -45,16 +68,22 @@ class MarkdownView extends React.Component { } render() { - const {content, renderers, renderContext, enableAnchorHeadings} = this.props; + const { + content, + renderers, + renderContext, + enableAnchorHeadings, + classes + } = this.props; const rendererFactory = binder.getExtension("markdown-renderer-factory"); let rendererList = renderers; - if (rendererFactory){ + if (rendererFactory) { rendererList = rendererFactory(renderContext); } - if (!rendererList){ + if (!rendererList) { rendererList = {}; } @@ -62,12 +91,12 @@ class MarkdownView extends React.Component { rendererList.heading = MarkdownHeadingRenderer; } - if (!rendererList.code){ + if (!rendererList.code) { rendererList.code = SyntaxHighlighter; } return ( -
    (this.contentRef = el)}> +
    (this.contentRef = el)}> { } } -export default withRouter(MarkdownView); +export default injectSheet(styles)(withRouter(MarkdownView)); diff --git a/scm-ui-components/packages/ui-components/src/Tag.js b/scm-ui-components/packages/ui-components/src/Tag.js new file mode 100644 index 0000000000..6ccdf0093f --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/Tag.js @@ -0,0 +1,60 @@ +//@flow +import * as React from "react"; +import classNames from "classnames"; + +type Props = { + className?: string, + color: string, + icon?: string, + label: string, + title?: string, + onClick?: () => void, + onRemove?: () => void +}; + +class Tag extends React.Component { + static defaultProps = { + color: "light" + }; + + render() { + const { + className, + color, + icon, + label, + title, + onClick, + onRemove + } = this.props; + let showIcon = null; + if (icon) { + showIcon = ( + <> + +   + + ); + } + let showDelete = null; + if (onRemove) { + showDelete = ; + } + + return ( + <> + + {showIcon} + {label} + + {showDelete} + + ); + } +} + +export default Tag; diff --git a/scm-ui-components/packages/ui-components/src/buttons/AddButton.js b/scm-ui-components/packages/ui-components/src/buttons/AddButton.js index 8f46fb66a4..b5ad8d23ff 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/AddButton.js +++ b/scm-ui-components/packages/ui-components/src/buttons/AddButton.js @@ -1,11 +1,11 @@ -//@flow -import React from "react"; -import Button, { type ButtonProps } from "./Button"; - -class AddButton extends React.Component { - render() { - return + ); + } + return ( ); - }; - + } } export default withRouter(Button); diff --git a/scm-ui-components/packages/ui-components/src/buttons/ButtonAddons.js b/scm-ui-components/packages/ui-components/src/buttons/ButtonAddons.js index 5b248d64af..4208b34294 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/ButtonAddons.js +++ b/scm-ui-components/packages/ui-components/src/buttons/ButtonAddons.js @@ -14,7 +14,7 @@ class ButtonAddons extends React.Component { const childWrapper = []; React.Children.forEach(children, child => { if (child) { - childWrapper.push(

    {child}

    ); + childWrapper.push(

    {child}

    ); } }); diff --git a/scm-ui-components/packages/ui-components/src/buttons/DeleteButton.js b/scm-ui-components/packages/ui-components/src/buttons/DeleteButton.js index ad67d50b5f..644b713f00 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/DeleteButton.js +++ b/scm-ui-components/packages/ui-components/src/buttons/DeleteButton.js @@ -4,7 +4,7 @@ import Button, { type ButtonProps } from "./Button"; class DeleteButton extends React.Component { render() { - return
    + icon={sideBySide ? "align-left" : "columns"} + label={t(sideBySide ? "diff.combined" : "diff.sideBySide")} + reducedMobile={true} + /> {fileControls}
    diff --git a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js index 57166ece9d..53d187959c 100644 --- a/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js +++ b/scm-ui-components/packages/ui-components/src/repos/changesets/ChangesetButtonGroup.js @@ -16,24 +16,22 @@ type Props = { class ChangesetButtonGroup extends React.Component { render() { const { repository, changeset, t } = this.props; - const changesetLink = createChangesetLink(repository, changeset); const sourcesLink = createSourcesLink(repository, changeset); - return ( - - + - +
    + {passwordChangeField}
    - {passwordChangeField} span:nth-child(2) { + display: none; + } + } + @media screen and (max-width: 1087px) and (min-width: 769px) { + // simultaneously with left margin of Bulma + > .icon:first-child:not(:last-child) { + margin: 0; + } + } + @media screen and (max-width: 768px) { + // simultaneously with left margin of Bulma + .icon:first-child:not(:last-child) { + margin-right: calc(-0.375em - 1px); + } + } + } } -// import at the end, because we need a lot of stuff from bulma/bulma .box-link-shadow { &:hover, &:focus { @@ -235,53 +421,7 @@ ul.is-separated { } } -.user { - display: inline-block; - font-weight: bold; -} - -// buttons -.button { - padding-left: 1.5em; - padding-right: 1.5em; - height: 2.5rem; - - &.is-primary { - background-color: #00d1df; - } - &.is-primary:hover, - &.is-primary.is-hovered { - background-color: #00b9c6; - } - &.is-primary:active, - &.is-primary.is-active { - background-color: #00a1ac; - } - &.is-primary[disabled] { - background-color: #40dde7; - } - &.reduced-mobile { - @media screen and (max-width: 1087px) { - > span:nth-child(2) { - display: none; - } - } - @media screen and (max-width: 1087px) and (min-width: 769px) { - // simultaneously with left margin of Bulma - > .icon:first-child:not(:last-child) { - margin: 0; - } - } - @media screen and (max-width: 768px) { - // simultaneously with left margin of Bulma - .icon:first-child:not(:last-child) { - margin-right: calc(-0.375em - 1px); - } - } - } -} - -// multiline Columns +// columns .columns.is-multiline { .column { height: 120px; @@ -416,7 +556,7 @@ ul.is-separated { .panel-heading { border: none; - border-bottom: 1px solid #dbdbdb; + border-bottom: 1px solid $border; border-radius: 0.25rem 0.25rem 0 0; > .field { @@ -427,14 +567,6 @@ ul.is-separated { .panel-block { display: block; border: none; - - & .comment-wrapper:first-child div:first-child { - border-top: none; - } - - & .diff-widget-content div { - border-bottom: none; - } } .panel-footer { @@ -445,7 +577,7 @@ ul.is-separated { line-height: 1.25; padding: 0.5em 0.75em; border: none; - border-top: 1px solid #dbdbdb; + border-top: 1px solid $border; border-radius: 0 0 0.25rem 0.25rem; } } @@ -470,10 +602,6 @@ form .field:not(.is-grouped) { } } -.is-icon { - color: $grey-light; -} - // label with help-icon compensation .label-icon-spacing { margin-top: 30px; @@ -499,14 +627,19 @@ form .field:not(.is-grouped) { .pagination-ellipsis { padding-left: 1.5em; padding-right: 1.5em; - height: 2.5rem; + height: 2.8rem; + min-width: 5rem; } .pagination-previous, .pagination-next { + height: 2.8rem; min-width: 6.75em; } +.pagination-link.is-current { + opacity: 1; +} -// dark hero colors +// hero .hero.is-dark { background-color: #002e4b; background-image: url(../images/scmManagerHero.jpg); @@ -528,7 +661,7 @@ form .field:not(.is-grouped) { background-color: whitesmoke; } -// sidebar menu +// aside .aside-background { bottom: 0; left: 50%; @@ -537,6 +670,8 @@ form .field:not(.is-grouped) { top: 0; background-color: whitesmoke; } + +// menu .menu { div { height: 100%; @@ -544,7 +679,6 @@ form .field:not(.is-grouped) { margin-bottom: 1rem; } } - .menu-label { color: #fff; font-size: 1em; @@ -614,17 +748,6 @@ form .field:not(.is-grouped) { } } -// modal -.modal { - .modal-card-foot { - justify-content: flex-end; // pulled-right - } -} - -.modal-card-body div div:last-child { - border-bottom: none; -} - .sub-menu li { line-height: 1; @@ -650,6 +773,17 @@ form .field:not(.is-grouped) { } } +// modal +.modal { + .modal-card-foot { + justify-content: flex-end; // pulled-right + } +} + +.modal-card-body div div:last-child { + border-bottom: none; +} + // cursor .has-cursor-pointer { cursor: pointer; 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);