From db540f5f0253d3ca3490e82bd65d3e92dbbd1733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 17 Feb 2020 16:48:14 +0100 Subject: [PATCH 01/53] Add limit parameter --- .../repository/api/BrowseCommandBuilder.java | 12 ++++++++++ .../repository/spi/BrowseCommandRequest.java | 24 +++++++++++++++++++ .../spi/FileBaseCommandRequest.java | 10 ++++++++ .../repository/spi/GitBrowseCommandTest.java | 13 ++++++++++ 4 files changed, 59 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index 563557f0c1..53281b52b8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -300,6 +300,18 @@ public final class BrowseCommandBuilder return this; } + /** + * Limit the number of result files to limit entries. + * + * @param limit The maximal number of files this request shall return. + * + * @since 2.0.0 + */ + public BrowseCommandBuilder setLimit(int limit) { + request.setLimit(limit); + return this; + } + private void updateCache(BrowserResult updatedResult) { if (!disableCache) { CacheKey key = new CacheKey(repository, request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 9c23fe93f2..c856421855 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -191,6 +191,17 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest this.recursive = recursive; } + /** + * Limit the number of result files to limit entries. + * + * @param limit The maximal number of files this request shall return. + * + * @since 2.0.0 + */ + public void setLimit(int limit) { + this.limit = limit; + } + //~--- get methods ---------------------------------------------------------- /** @@ -232,6 +243,15 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest return recursive; } + /** + * Returns the limit for the number of result files. + * + * @since 2.0.0 + */ + public int getLimit() { + return limit; + } + public void updateCache(BrowserResult update) { if (updater != null) { updater.accept(update); @@ -249,6 +269,10 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** browse file objects recursive */ private boolean recursive = false; + + /** Limit the number of result files to limit entries. */ + private int limit = 1000; + // WARNING / TODO: This field creates a reverse channel from the implementation to the API. This will break // whenever the API runs in a different process than the SPI (for example to run explicit hosts for git repositories). private final transient Consumer updater; diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 9f563345fd..0a2192897a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,6 +147,10 @@ public abstract class FileBaseCommandRequest this.revision = revision; } + public void setLimit(int limit) { + this.limit = limit; + } + //~--- get methods ---------------------------------------------------------- /** @@ -171,6 +175,10 @@ public abstract class FileBaseCommandRequest return revision; } + public int getLimit() { + return limit; + } + //~--- methods -------------------------------------------------------------- /** @@ -208,4 +216,6 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; + + private int limit; } 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 faa8f0c2fa..e45ad0d04b 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 @@ -236,6 +236,19 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { .containsExactly(of(42L)); } + @Test + public void testBrowseLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + FileObject root = createCommand() + .getBrowserResult(request).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + assertThat(foList).hasSize(2); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 8a1a43fcc50fd53f657e00046670a5ea6b4179c4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 08:08:09 +0100 Subject: [PATCH 02/53] Document parameter --- .../java/sonia/scm/repository/api/BrowseCommandBuilder.java | 5 ++++- .../java/sonia/scm/repository/spi/BrowseCommandRequest.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index 53281b52b8..e088248762 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -301,7 +301,10 @@ public final class BrowseCommandBuilder } /** - * Limit the number of result files to limit entries. + * Limit the number of result files to limit entries. By default this is set to + * {@value BrowseCommandRequest#DEFAULT_REQUEST_LIMIT}. Be aware that this parameter can have + * severe performance implications. Reading a repository with thousands of files in one folder + * can generate a huge load for a longer time. * * @param limit The maximal number of files this request shall return. * diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index c856421855..e7181829a1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -49,7 +49,8 @@ import java.util.function.Consumer; public final class BrowseCommandRequest extends FileBaseCommandRequest { - /** Field description */ + public static final int DEFAULT_REQUEST_LIMIT = 1000; + private static final long serialVersionUID = 7956624623516803183L; public BrowseCommandRequest() { @@ -271,7 +272,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** Limit the number of result files to limit entries. */ - private int limit = 1000; + private int limit = DEFAULT_REQUEST_LIMIT; // WARNING / TODO: This field creates a reverse channel from the implementation to the API. This will break // whenever the API runs in a different process than the SPI (for example to run explicit hosts for git repositories). From f68423a5d80bb7728914c5e328328e1ee32c3276 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 08:22:36 +0100 Subject: [PATCH 03/53] Implement request limit for git --- .../java/sonia/scm/repository/spi/GitBrowseCommand.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 e8ef5a7a33..5158a74ecb 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 @@ -110,6 +110,8 @@ public class GitBrowseCommand extends AbstractGitCommand private BrowserResult browserResult; + private int resultCount = 0; + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { super(context, repository); this.lfsBlobStoreFactory = lfsBlobStoreFactory; @@ -251,7 +253,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next()) + while (treeWalk.next() && resultCount < request.getLimit()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -262,6 +264,8 @@ public class GitBrowseCommand extends AbstractGitCommand files.add(fileObject); + ++resultCount; + if (request.isRecursive() && fileObject.isDirectory()) { treeWalk.enterSubtree(); FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); From 67e58209cfbc45989e8956659d58683ef2b6fbdb Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 09:57:57 +0100 Subject: [PATCH 04/53] Implement proceed from for git --- .../repository/api/BrowseCommandBuilder.java | 12 +++++++++++ .../repository/spi/BrowseCommandRequest.java | 21 +++++++++++++++++++ .../scm/repository/spi/GitBrowseCommand.java | 6 ++++-- .../repository/spi/GitBrowseCommandTest.java | 14 +++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index e088248762..7862fdb64a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -315,6 +315,18 @@ public final class BrowseCommandBuilder return this; } + /** + * Proceed the list from the given number on (zero based). + * + * @param proceedFrom The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. + * @since 2.0.0 + */ + public BrowseCommandBuilder setProceedFrom(int proceedFrom) { + request.setProceedFrom(proceedFrom); + return this; + } + private void updateCache(BrowserResult updatedResult) { if (!disableCache) { CacheKey key = new CacheKey(repository, request); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index e7181829a1..3dbe71f322 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -52,6 +52,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public static final int DEFAULT_REQUEST_LIMIT = 1000; private static final long serialVersionUID = 7956624623516803183L; + private int proceedFrom; public BrowseCommandRequest() { this(null); @@ -203,6 +204,17 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest this.limit = limit; } + /** + * Proceed the list from the given number on (zero based). + * + * @param proceedFrom The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. + * @since 2.0.0 + */ + public void setProceedFrom(int proceedFrom) { + this.proceedFrom = proceedFrom; + } + //~--- get methods ---------------------------------------------------------- /** @@ -253,6 +265,15 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest return limit; } + /** + * The number of the entry, the result start with. All preceding entries will be omitted. + * + * @since 2.0.0 + */ + public int getProceedFrom() { + return proceedFrom; + } + public void updateCache(BrowserResult update) { if (updater != null) { updater.accept(update); 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 5158a74ecb..79aeb51342 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 @@ -253,7 +253,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next() && resultCount < request.getLimit()) + while (treeWalk.next() && resultCount < request.getLimit() + request.getProceedFrom()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -262,7 +262,9 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - files.add(fileObject); + if (resultCount >= request.getProceedFrom()) { + files.add(fileObject); + } ++resultCount; 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 e45ad0d04b..78aa12e8e0 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 @@ -249,6 +249,20 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList).hasSize(2); } + @Test + public void testBrowseProceedFrom() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + request.setProceedFrom(2); + FileObject root = createCommand() + .getBrowserResult(request).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").contains("c", "f.txt"); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 9e7fd52f9de15ddbacb9f9efc94dd54d7eef3ec4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 10:47:20 +0100 Subject: [PATCH 05/53] Implement request limit for hg --- .../scm/repository/spi/HgBrowseCommand.java | 3 + .../spi/javahg/HgFileviewCommand.java | 30 +++++++++- .../resources/sonia/scm/hg/ext/fileview.py | 57 ++++++++++++------- .../repository/spi/HgBrowseCommandTest.java | 28 ++++++++- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index b3e8e89a5f..6f2e519d3d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -101,6 +101,9 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand cmd.disableSubRepositoryDetection(); } + cmd.setLimit(request.getLimit()); + cmd.setProceedFrom(request.getProceedFrom()); + FileObject file = cmd.execute(); return new BrowserResult(c == null? "tip": c.getNode(), revision, file); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 4d5d5e8646..3b74f6b9cb 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -141,6 +141,35 @@ public class HgFileviewCommand extends AbstractCommand return this; } + /** + * Limit the number of result files to limit entries. + * + * @param limit The maximal number of files this request shall return. + * + * @return {@code this} + * @since 2.0.0 + */ + public HgFileviewCommand setLimit(int limit) { + cmdAppend("-l", limit); + + return this; + } + + /** + * Proceed the list from the given number on (zero based). + * + * @param proceedFrom The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. + * + * @return {@code this} + * @since 2.0.0 + */ + public HgFileviewCommand setProceedFrom(int proceedFrom) { + cmdAppend("-f", proceedFrom); + + return this; + } + /** * Executes the mercurial command and parses the output. * @@ -294,5 +323,4 @@ public class HgFileviewCommand extends AbstractCommand { return HgFileviewExtension.NAME; } - } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 1871200389..ec57192666 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -197,37 +197,43 @@ def collect_sub_repositories(revCtx): class File_Printer: - def __init__(self, ui, repo, revCtx, disableLastCommit, transport): + def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, proceedFrom): self.ui = ui self.repo = repo self.revCtx = revCtx self.disableLastCommit = disableLastCommit self.transport = transport + self.result_count = -1 + self.limit = limit + self.proceedFrom = proceedFrom def print_directory(self, path): - format = '%s/\n' - if self.transport: - format = 'd%s/\0' - self.ui.write( format % path) + if self.shouldPrintResult(): + format = '%s/\n' + if self.transport: + format = 'd%s/\0' + self.ui.write( format % path) def print_file(self, path): - file = self.revCtx[path] - date = '0 0' - description = 'n/a' - if not self.disableLastCommit: - linkrev = self.repo[file.linkrev()] - date = '%d %d' % _parsedate(linkrev.date()) - description = linkrev.description() - format = '%s %i %s %s\n' - if self.transport: - format = 'f%s\n%i %s %s\0' - self.ui.write( format % (file.path(), file.size(), date, description) ) + if self.shouldPrintResult(): + file = self.revCtx[path] + date = '0 0' + description = 'n/a' + if not self.disableLastCommit: + linkrev = self.repo[file.linkrev()] + date = '%d %d' % _parsedate(linkrev.date()) + description = linkrev.description() + format = '%s %i %s %s\n' + if self.transport: + format = 'f%s\n%i %s %s\0' + self.ui.write( format % (file.path(), file.size(), date, description) ) def print_sub_repository(self, path, subrepo): - format = '%s/ %s %s\n' - if self.transport: - format = 's%s/\n%s %s\0' - self.ui.write( format % (path, subrepo.revision, subrepo.url)) + if self.shouldPrintResult(): + format = '%s/ %s %s\n' + if self.transport: + format = 's%s/\n%s %s\0' + self.ui.write( format % (path, subrepo.revision, subrepo.url)) def visit(self, file): if file.sub_repository: @@ -237,6 +243,13 @@ class File_Printer: else: self.print_file(file.path) + def shouldPrintResult(self): + # The first result is the selected path (or root if not specified). This + # always has to be printed. Therefore we start counting with -1. + self.result_count += 1 + return self.result_count == 0 or self.proceedFrom < self.result_count <= self.limit + self.proceedFrom + + class File_Viewer: def __init__(self, revCtx, visitor): self.revCtx = revCtx @@ -271,13 +284,15 @@ class File_Viewer: ('d', 'disableLastCommit', False, 'disables last commit description and date'), ('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'), ('t', 'transport', False, 'format the output for command server'), + ('l', 'limit', 1000, 'limit the number of results'), + ('f', 'proceedFrom', 0, 'proceed from the given result number (zero based)'), ]) def fileview(ui, repo, **opts): revCtx = scmutil.revsingle(repo, opts["revision"]) subrepos = {} if not opts["disableSubRepositoryDetection"]: subrepos = collect_sub_repositories(revCtx) - printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"]) + printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["proceedFrom"]) viewer = File_Viewer(revCtx, printer) viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 92a05a05a0..e6c07983bf 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -45,7 +45,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -181,6 +180,33 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertEquals(2, c.getChildren().size()); } + @Test + public void testLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + + BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); + FileObject root = result.getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + } + + @Test + public void testProceedFrom() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(2); + request.setProceedFrom(2); + + BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); + FileObject root = result.getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + } + //~--- get methods ---------------------------------------------------------- /** From 9afc3a958073dfbde38b504cad475f366ac8331f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 12:56:20 +0100 Subject: [PATCH 06/53] Implement request limit for svn --- .../scm/repository/spi/SvnBrowseCommand.java | 11 ++- .../repository/spi/SvnBrowseCommandTest.java | 67 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index e4a32c8ca6..413c5765b0 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -52,6 +52,7 @@ import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; import java.util.Collection; +import java.util.Iterator; import static org.tmatesoft.svn.core.SVNErrorCode.FS_NO_SUCH_REVISION; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -73,6 +74,8 @@ public class SvnBrowseCommand extends AbstractSvnCommand private static final Logger logger = LoggerFactory.getLogger(SvnBrowseCommand.class); + private int resultCount = 0; + SvnBrowseCommand(SvnContext context, Repository repository) { super(context, repository); @@ -128,11 +131,13 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); - for (SVNDirEntry entry : entries) - { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getProceedFrom() && iterator.hasNext(); ++resultCount) { + SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - parent.addChild(child); + if (resultCount >= request.getProceedFrom()) { + parent.addChild(child); + } if (child.isDirectory() && request.isRecursive()) { traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 980d486b5c..0244e9dbeb 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -40,10 +40,10 @@ import sonia.scm.repository.FileObject; import java.io.IOException; import java.util.Collection; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -65,7 +65,17 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase @Test public void testBrowse() { - Collection foList = getRootFromTip(new BrowseCommandRequest()); + BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest()); + + assertNotNull(result); + + Collection foList1 = result.getFile().getChildren(); + + assertNotNull(foList1); + assertFalse(foList1.isEmpty()); + assertEquals(2, foList1.size()); + + Collection foList = foList1; FileObject a = getFileObject(foList, "a.txt"); FileObject c = getFileObject(foList, "c"); @@ -140,14 +150,24 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase request.setDisableLastCommit(true); - Collection foList = getRootFromTip(request); + BrowserResult result = createCommand().getBrowserResult(request); + + assertNotNull(result); + + Collection foList1 = result.getFile().getChildren(); + + assertNotNull(foList1); + assertFalse(foList1.isEmpty()); + assertEquals(2, foList1.size()); + + Collection foList = foList1; FileObject a = getFileObject(foList, "a.txt"); assertFalse(a.getDescription().isPresent()); assertFalse(a.getCommitDate().isPresent()); } - + @Test public void testRecursive() { BrowseCommandRequest request = new BrowseCommandRequest(); @@ -168,6 +188,32 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertEquals(2, c.getChildren().size()); } + @Test + public void testLimit() { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(1); + BrowserResult result = createCommand().getBrowserResult(request); + + assertNotNull(result); + + Collection foList = result.getFile().getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + } + + @Test + public void testProceedFrom() { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setProceedFrom(1); + BrowserResult result = createCommand().getBrowserResult(request); + + assertNotNull(result); + + Collection foList = result.getFile().getChildren(); + + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c"); + } + /** * Method description * @@ -198,17 +244,4 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase .orElseThrow(() -> new AssertionError("file " + name + " not found")); } - private Collection getRootFromTip(BrowseCommandRequest request) { - BrowserResult result = createCommand().getBrowserResult(request); - - assertNotNull(result); - - Collection foList = result.getFile().getChildren(); - - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(2, foList.size()); - - return foList; - } } From 3652a33fa001a487579e5115c6d90d2ba8e6d315 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 15:24:32 +0100 Subject: [PATCH 07/53] Add truncated flag for git --- .../main/java/sonia/scm/repository/FileObject.java | 12 +++++++++++- .../sonia/scm/repository/spi/GitBrowseCommand.java | 11 +++++++---- .../scm/repository/spi/GitBrowseCommandTest.java | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java index 8f1cf298de..e262177e19 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -87,7 +87,7 @@ public class FileObject implements LastModifiedAware, Serializable final FileObject other = (FileObject) obj; //J- - return Objects.equal(name, other.name) + return Objects.equal(name, other.name) && Objects.equal(path, other.path) && Objects.equal(directory, other.directory) && Objects.equal(description, other.description) @@ -282,6 +282,10 @@ public class FileObject implements LastModifiedAware, Serializable return computationAborted; } + public boolean isTruncated() { + return truncated; + } + //~--- set methods ---------------------------------------------------------- /** @@ -403,6 +407,10 @@ public class FileObject implements LastModifiedAware, Serializable this.children.add(child); } + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } + //~--- fields --------------------------------------------------------------- /** file description */ @@ -435,4 +443,6 @@ public class FileObject implements LastModifiedAware, Serializable /** Children of this file (aka directory). */ private Collection children = new ArrayList<>(); + + private boolean truncated; } 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 79aeb51342..5ececf176f 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 @@ -153,6 +153,7 @@ public class GitBrowseCommand extends AbstractGitCommand fileObject.setName(""); fileObject.setPath(""); fileObject.setDirectory(true); + fileObject.setTruncated(false); return fileObject; } @@ -253,7 +254,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next() && resultCount < request.getLimit() + request.getProceedFrom()) + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getProceedFrom()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -262,12 +263,10 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - if (resultCount >= request.getProceedFrom()) { + if (resultCount > request.getProceedFrom()) { files.add(fileObject); } - ++resultCount; - if (request.isRecursive() && fileObject.isDirectory()) { treeWalk.enterSubtree(); FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); @@ -279,6 +278,10 @@ public class GitBrowseCommand extends AbstractGitCommand parent.setChildren(files); + if (resultCount > request.getLimit() + request.getProceedFrom()) { + parent.setTruncated(true); + } + return null; } 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 78aa12e8e0..04d2af9762 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 @@ -72,6 +72,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { BrowserResult result = createCommand().getBrowserResult(request); FileObject fileObject = result.getFile(); assertEquals("a.txt", fileObject.getName()); + assertFalse(fileObject.isTruncated()); } @Test @@ -247,6 +248,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).hasSize(2); + assertTrue(root.isTruncated()); } @Test @@ -261,6 +263,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).extracting("name").contains("c", "f.txt"); + assertFalse(root.isTruncated()); } private FileObject findFile(Collection foList, String name) { From 6eca277d65eb75a34439227e59042aa47b84ae65 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 15:35:49 +0100 Subject: [PATCH 08/53] Add truncated flag for hg --- .../scm/repository/spi/javahg/HgFileviewCommand.java | 6 +++++- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 10 ++++++++++ .../sonia/scm/repository/spi/HgBrowseCommandTest.java | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 3b74f6b9cb..9ad2995307 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -60,6 +60,7 @@ import java.util.LinkedList; public class HgFileviewCommand extends AbstractCommand { + public static final char TRUNCATED_MARK = 't'; private boolean disableLastCommit = false; private HgFileviewCommand(Repository repository) @@ -186,7 +187,7 @@ public class HgFileviewCommand extends AbstractCommand HgInputStream stream = launchStream(); FileObject last = null; - while (stream.peek() != -1) { + while (stream.peek() != -1 && stream.peek() != TRUNCATED_MARK) { FileObject file = read(stream); while (!stack.isEmpty()) { @@ -210,6 +211,9 @@ public class HgFileviewCommand extends AbstractCommand return last; } else { // if the stack is not empty, the requested path is a directory + if (stream.read() == TRUNCATED_MARK) { + stack.getLast().setTruncated(true); + } return stack.getLast(); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index ec57192666..7c36a55703 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -249,6 +249,15 @@ class File_Printer: self.result_count += 1 return self.result_count == 0 or self.proceedFrom < self.result_count <= self.limit + self.proceedFrom + def isTruncated(self): + return self.result_count > self.limit + self.proceedFrom + + def finish(self): + if self.isTruncated(): + if self.transport: + self.ui.write( "t") + else: + self.ui.write("truncated") class File_Viewer: def __init__(self, revCtx, visitor): @@ -297,3 +306,4 @@ def fileview(ui, repo, **opts): viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos viewer.view(opts["path"]) + printer.finish() diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index e6c07983bf..3168738252 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -191,6 +191,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + assertThat(root.isTruncated()).isTrue(); } @Test @@ -205,6 +206,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + assertThat(root.isTruncated()).isFalse(); } //~--- get methods ---------------------------------------------------------- From 1c8088a1c62030557af2f6f8b6c17dfbcd6d737d Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 15:55:01 +0100 Subject: [PATCH 09/53] Add truncated flag for svn --- .../main/java/sonia/scm/repository/spi/SvnBrowseCommand.java | 3 +++ .../java/sonia/scm/repository/spi/SvnBrowseCommandTest.java | 1 + 2 files changed, 4 insertions(+) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 413c5765b0..7147b14a57 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -143,6 +143,9 @@ public class SvnBrowseCommand extends AbstractSvnCommand traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } } + if (resultCount >= request.getLimit() + request.getProceedFrom()) { + parent.setTruncated(true); + } } private String createBasePath(String path) diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 0244e9dbeb..ecf3ee0893 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -199,6 +199,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + assertThat(result.getFile().isTruncated()).isTrue(); } @Test From 6b3f36e7ea5fe10af8a49762016e804dd1593794 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 18 Feb 2020 17:56:22 +0100 Subject: [PATCH 10/53] WIP --- .../repository/spi/BrowseCommandRequest.java | 2 +- .../src/repos/sources/components/FileTree.tsx | 47 ++++++++++++------- .../v2/resources/BaseFileObjectDtoMapper.java | 6 ++- .../BrowserResultToFileObjectDtoMapper.java | 15 ++++-- .../scm/api/v2/resources/FileObjectDto.java | 2 +- .../api/v2/resources/SourceRootResource.java | 20 ++++---- ...rowserResultToFileObjectDtoMapperTest.java | 6 +-- 7 files changed, 62 insertions(+), 36 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 3dbe71f322..e9bdde76a9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -49,7 +49,7 @@ import java.util.function.Consumer; public final class BrowseCommandRequest extends FileBaseCommandRequest { - public static final int DEFAULT_REQUEST_LIMIT = 1000; + public static final int DEFAULT_REQUEST_LIMIT = 100; private static final long serialVersionUID = 7956624623516803183L; private int proceedFrom; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 1a5a8b5b58..5163bc150a 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -9,6 +9,7 @@ import { File, Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; import { fetchSources, getFetchSourcesFailure, getSources, isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; +import queryString from "query-string"; type Props = WithTranslation & { loading: boolean; @@ -18,6 +19,7 @@ type Props = WithTranslation & { revision: string; path: string; baseUrl: string; + location: any; updateSources: () => void; @@ -85,7 +87,7 @@ class FileTree extends React.Component { } renderSourcesTable() { - const { tree, revision, path, baseUrl, t } = this.props; + const { tree, revision, path, baseUrl, t, location } = this.props; const files = []; @@ -119,6 +121,7 @@ class FileTree extends React.Component { } if (files && files.length > 0) { + let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); @@ -126,24 +129,32 @@ class FileTree extends React.Component { baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); } + const proceedFrom = queryString.parse(location.search).proceedFrom; + if (proceedFrom) { + baseUrlWithRevision += "?proceedFrom=" + proceedFrom; + } + return ( - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map(file => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
+ <> + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
+ {tree.truncated &&

TRUNCATED

} + ); } return {t("sources.noSources")}; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java index bb74bc5045..9260343065 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -10,6 +10,7 @@ import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.SubRepository; +import sonia.scm.repository.spi.BrowseCommandRequest; import javax.inject.Inject; @@ -30,7 +31,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); @ObjectFactory - FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, FileObject fileObject) { + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom, FileObject fileObject) { String path = removeFirstSlash(fileObject.getPath()); Links.Builder links = Links.linkingTo(); if (fileObject.isDirectory()) { @@ -39,6 +40,9 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); } + if (fileObject.isTruncated()) { + links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?proceedFrom=" + (proceedFrom + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); + } Embedded.Builder embeddedBuilder = embeddedBuilder(); applyEnrichers(links, embeddedBuilder, namespaceAndName, browserResult, fileObject); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java index f9304881e7..4ff1002367 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java @@ -1,14 +1,19 @@ package sonia.scm.api.v2.resources; +import com.google.common.annotations.VisibleForTesting; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.ObjectFactory; import org.mapstruct.Qualifier; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.SubRepository; +import sonia.scm.repository.spi.BrowseCommand; +import sonia.scm.repository.spi.BrowseCommandRequest; import javax.inject.Inject; import java.lang.annotation.ElementType; @@ -19,19 +24,23 @@ import java.time.Instant; import java.util.Optional; import java.util.OptionalLong; +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; + @Mapper public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectDtoMapper { - FileObjectDto map(BrowserResult browserResult, @Context NamespaceAndName namespaceAndName) { - FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult); + FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, int proceedFrom) { + FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, proceedFrom); fileObjectDto.setRevision(browserResult.getRevision()); + return fileObjectDto; } @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "children", qualifiedBy = Children.class) @Children - protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult); + protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom); @Override void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java index b273f241dc..1d39c90f37 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -15,7 +15,6 @@ import java.util.OptionalLong; @Getter @Setter -@NoArgsConstructor public class FileObjectDto extends HalRepresentation { private String name; private String path; @@ -31,6 +30,7 @@ public class FileObjectDto extends HalRepresentation { private String revision; private boolean partialResult; private boolean computationAborted; + private boolean truncated; public FileObjectDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 758afd7660..10f7cfd47d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -8,11 +8,12 @@ import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.core.Response; +import javax.ws.rs.QueryParam; import java.io.IOException; import java.net.URLDecoder; @@ -34,36 +35,37 @@ public class SourceRootResource { @GET @Produces(VndMediaType.SOURCE) @Path("") - public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { - return getSource(namespace, name, "/", null); + public FileObjectDto getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { + return getSource(namespace, name, "/", null, proceedFrom); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}") - public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException { - return getSource(namespace, name, "/", revision); + public FileObjectDto getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { + return getSource(namespace, name, "/", revision, proceedFrom); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}/{path: .*}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws IOException { - return getSource(namespace, name, path, revision); + public FileObjectDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { + return getSource(namespace, name, path, revision, proceedFrom); } - private Response getSource(String namespace, String repoName, String path, String revision) throws IOException { + private FileObjectDto getSource(String namespace, String repoName, String path, String revision, int proceedFrom) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand(); browseCommand.setPath(path); + browseCommand.setProceedFrom(proceedFrom); if (revision != null && !revision.isEmpty()) { browseCommand.setRevision(URLDecoder.decode(revision, "UTF-8")); } BrowserResult browserResult = browseCommand.getBrowserResult(); if (browserResult != null) { - return Response.ok(browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName)).build(); + return browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName, proceedFrom); } else { throw notFound(entity("Source", path).in("Revision", revision).in(namespaceAndName)); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java index 273cc25018..dc264530fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapperTest.java @@ -66,7 +66,7 @@ public class BrowserResultToFileObjectDtoMapperTest { public void shouldMapAttributesCorrectly() { BrowserResult browserResult = createBrowserResult(); - FileObjectDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar")); + FileObjectDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar"), 0); assertEqualAttributes(browserResult, dto); } @@ -76,7 +76,7 @@ public class BrowserResultToFileObjectDtoMapperTest { BrowserResult browserResult = createBrowserResult(); NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar"); - FileObjectDto dto = mapper.map(browserResult, namespaceAndName); + FileObjectDto dto = mapper.map(browserResult, namespaceAndName, 0); assertThat(dto.getEmbedded().getItemsBy("children")).hasSize(2); } @@ -86,7 +86,7 @@ public class BrowserResultToFileObjectDtoMapperTest { BrowserResult browserResult = createBrowserResult(); NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar"); - FileObjectDto dto = mapper.map(browserResult, namespaceAndName); + FileObjectDto dto = mapper.map(browserResult, namespaceAndName, 0); assertThat(dto.getLinks().getLinkBy("self").get().getHref()).contains("path"); } From fe1591171dd68081ca5740a2464462e370a0620a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 19 Feb 2020 09:23:23 +0100 Subject: [PATCH 11/53] Rename 'proceedFrom' to 'offset' --- .../repository/api/BrowseCommandBuilder.java | 8 ++++---- .../repository/spi/BrowseCommandRequest.java | 14 +++++++------- .../scm/repository/spi/GitBrowseCommand.java | 6 +++--- .../repository/spi/GitBrowseCommandTest.java | 4 ++-- .../scm/repository/spi/HgBrowseCommand.java | 2 +- .../spi/javahg/HgFileviewCommand.java | 8 ++++---- .../resources/sonia/scm/hg/ext/fileview.py | 12 ++++++------ .../repository/spi/HgBrowseCommandTest.java | 4 ++-- .../scm/repository/spi/SvnBrowseCommand.java | 6 +++--- .../repository/spi/SvnBrowseCommandTest.java | 4 ++-- .../src/repos/sources/components/FileTree.tsx | 6 +++--- .../v2/resources/BaseFileObjectDtoMapper.java | 4 ++-- .../BrowserResultToFileObjectDtoMapper.java | 6 +++--- .../api/v2/resources/SourceRootResource.java | 18 +++++++++--------- 14 files changed, 51 insertions(+), 51 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index 7862fdb64a..c312b20534 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -318,12 +318,12 @@ public final class BrowseCommandBuilder /** * Proceed the list from the given number on (zero based). * - * @param proceedFrom The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. * @since 2.0.0 */ - public BrowseCommandBuilder setProceedFrom(int proceedFrom) { - request.setProceedFrom(proceedFrom); + public BrowseCommandBuilder setOffset(int offset) { + request.setOffset(offset); return this; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index e9bdde76a9..70dec0980a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -52,7 +52,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public static final int DEFAULT_REQUEST_LIMIT = 100; private static final long serialVersionUID = 7956624623516803183L; - private int proceedFrom; + private int offset; public BrowseCommandRequest() { this(null); @@ -207,12 +207,12 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest /** * Proceed the list from the given number on (zero based). * - * @param proceedFrom The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. * @since 2.0.0 */ - public void setProceedFrom(int proceedFrom) { - this.proceedFrom = proceedFrom; + public void setOffset(int offset) { + this.offset = offset; } //~--- get methods ---------------------------------------------------------- @@ -270,8 +270,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest * * @since 2.0.0 */ - public int getProceedFrom() { - return proceedFrom; + public int getOffset() { + return offset; } public void updateCache(BrowserResult update) { 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 5ececf176f..f53b7ec0a4 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 @@ -254,7 +254,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List files = Lists.newArrayList(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getProceedFrom()) + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { FileObject fileObject = createFileObject(repo, request, revId, treeWalk); @@ -263,7 +263,7 @@ public class GitBrowseCommand extends AbstractGitCommand return fileObject; } - if (resultCount > request.getProceedFrom()) { + if (resultCount > request.getOffset()) { files.add(fileObject); } @@ -278,7 +278,7 @@ public class GitBrowseCommand extends AbstractGitCommand parent.setChildren(files); - if (resultCount > request.getLimit() + request.getProceedFrom()) { + if (resultCount > request.getLimit() + request.getOffset()) { parent.setTruncated(true); } 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 04d2af9762..ce8214e2fa 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 @@ -252,10 +252,10 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } @Test - public void testBrowseProceedFrom() throws IOException { + public void testBrowseOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setLimit(2); - request.setProceedFrom(2); + request.setOffset(2); FileObject root = createCommand() .getBrowserResult(request).getFile(); assertNotNull(root); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java index 6f2e519d3d..63a2b0f4cd 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java @@ -102,7 +102,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand } cmd.setLimit(request.getLimit()); - cmd.setProceedFrom(request.getProceedFrom()); + cmd.setOffset(request.getOffset()); FileObject file = cmd.execute(); return new BrowserResult(c == null? "tip": c.getNode(), revision, file); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 9ad2995307..815e830c8a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -159,14 +159,14 @@ public class HgFileviewCommand extends AbstractCommand /** * Proceed the list from the given number on (zero based). * - * @param proceedFrom The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the entry, the result should start with (zero based). + * All preceding entries will be omitted. * * @return {@code this} * @since 2.0.0 */ - public HgFileviewCommand setProceedFrom(int proceedFrom) { - cmdAppend("-f", proceedFrom); + public HgFileviewCommand setOffset(int offset) { + cmdAppend("-o", offset); return this; } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 7c36a55703..86e6175d55 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -197,7 +197,7 @@ def collect_sub_repositories(revCtx): class File_Printer: - def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, proceedFrom): + def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, offset): self.ui = ui self.repo = repo self.revCtx = revCtx @@ -205,7 +205,7 @@ class File_Printer: self.transport = transport self.result_count = -1 self.limit = limit - self.proceedFrom = proceedFrom + self.offset = offset def print_directory(self, path): if self.shouldPrintResult(): @@ -247,10 +247,10 @@ class File_Printer: # The first result is the selected path (or root if not specified). This # always has to be printed. Therefore we start counting with -1. self.result_count += 1 - return self.result_count == 0 or self.proceedFrom < self.result_count <= self.limit + self.proceedFrom + return self.result_count == 0 or self.offset < self.result_count <= self.limit + self.offset def isTruncated(self): - return self.result_count > self.limit + self.proceedFrom + return self.result_count > self.limit + self.offset def finish(self): if self.isTruncated(): @@ -294,14 +294,14 @@ class File_Viewer: ('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'), ('t', 'transport', False, 'format the output for command server'), ('l', 'limit', 1000, 'limit the number of results'), - ('f', 'proceedFrom', 0, 'proceed from the given result number (zero based)'), + ('o', 'offset', 0, 'proceed from the given result number (zero based)'), ]) def fileview(ui, repo, **opts): revCtx = scmutil.revsingle(repo, opts["revision"]) subrepos = {} if not opts["disableSubRepositoryDetection"]: subrepos = collect_sub_repositories(revCtx) - printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["proceedFrom"]) + printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["offset"]) viewer = File_Viewer(revCtx, printer) viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 3168738252..f74d036843 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -195,10 +195,10 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { } @Test - public void testProceedFrom() throws IOException { + public void testOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); request.setLimit(2); - request.setProceedFrom(2); + request.setOffset(2); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); FileObject root = result.getFile(); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 7147b14a57..1daaf5af61 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -131,11 +131,11 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); - for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getProceedFrom() && iterator.hasNext(); ++resultCount) { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - if (resultCount >= request.getProceedFrom()) { + if (resultCount >= request.getOffset()) { parent.addChild(child); } @@ -143,7 +143,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } } - if (resultCount >= request.getLimit() + request.getProceedFrom()) { + if (resultCount >= request.getLimit() + request.getOffset()) { parent.setTruncated(true); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index ecf3ee0893..589f0d4107 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -203,9 +203,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase } @Test - public void testProceedFrom() { + public void testOffset() { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setProceedFrom(1); + request.setOffset(1); BrowserResult result = createCommand().getBrowserResult(request); assertNotNull(result); diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 5163bc150a..0c5568d09c 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -129,9 +129,9 @@ class FileTree extends React.Component { baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); } - const proceedFrom = queryString.parse(location.search).proceedFrom; - if (proceedFrom) { - baseUrlWithRevision += "?proceedFrom=" + proceedFrom; + const offset = queryString.parse(location.search).offset; + if (offset) { + baseUrlWithRevision += "?offset=" + offset; } return ( diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java index 9260343065..be9621f0b7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -31,7 +31,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); @ObjectFactory - FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom, FileObject fileObject) { + FileObjectDto createDto(@Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer offset, FileObject fileObject) { String path = removeFirstSlash(fileObject.getPath()); Links.Builder links = Links.linkingTo(); if (fileObject.isDirectory()) { @@ -41,7 +41,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); } if (fileObject.isTruncated()) { - links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?proceedFrom=" + (proceedFrom + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); + links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?offset=" + (offset + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); } Embedded.Builder embeddedBuilder = embeddedBuilder(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java index 4ff1002367..0b069fcbe4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BrowserResultToFileObjectDtoMapper.java @@ -30,8 +30,8 @@ import static de.otto.edison.hal.Link.link; @Mapper public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectDtoMapper { - FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, int proceedFrom) { - FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, proceedFrom); + FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, int offset) { + FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, offset); fileObjectDto.setRevision(browserResult.getRevision()); return fileObjectDto; @@ -40,7 +40,7 @@ public abstract class BrowserResultToFileObjectDtoMapper extends BaseFileObjectD @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "children", qualifiedBy = Children.class) @Children - protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer proceedFrom); + protected abstract FileObjectDto fileObjectToDto(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context BrowserResult browserResult, @Context Integer offset); @Override void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 10f7cfd47d..d189b40b93 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -35,37 +35,37 @@ public class SourceRootResource { @GET @Produces(VndMediaType.SOURCE) @Path("") - public FileObjectDto getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { - return getSource(namespace, name, "/", null, proceedFrom); + public FileObjectDto getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("offset") int offset) throws IOException { + return getSource(namespace, name, "/", null, offset); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}") - public FileObjectDto getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { - return getSource(namespace, name, "/", revision, proceedFrom); + public FileObjectDto getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @DefaultValue("0") @QueryParam("offset") int offset) throws IOException { + return getSource(namespace, name, "/", revision, offset); } @GET @Produces(VndMediaType.SOURCE) @Path("{revision}/{path: .*}") - public FileObjectDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("proceedFrom") int proceedFrom) throws IOException { - return getSource(namespace, name, path, revision, proceedFrom); + public FileObjectDto get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, @DefaultValue("0") @QueryParam("offset") int offset) throws IOException { + return getSource(namespace, name, path, revision, offset); } - private FileObjectDto getSource(String namespace, String repoName, String path, String revision, int proceedFrom) throws IOException { + private FileObjectDto getSource(String namespace, String repoName, String path, String revision, int offset) throws IOException { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName); try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand(); browseCommand.setPath(path); - browseCommand.setProceedFrom(proceedFrom); + browseCommand.setOffset(offset); if (revision != null && !revision.isEmpty()) { browseCommand.setRevision(URLDecoder.decode(revision, "UTF-8")); } BrowserResult browserResult = browseCommand.getBrowserResult(); if (browserResult != null) { - return browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName, proceedFrom); + return browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName, offset); } else { throw notFound(entity("Source", path).in("Revision", revision).in(namespaceAndName)); } From 4bc3d16aa958e8d3232b6cca255d8e1a31079b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 19 Feb 2020 18:37:09 +0100 Subject: [PATCH 12/53] WIP --- scm-ui/ui-types/src/Sources.ts | 1 + .../src/repos/sources/components/FileTree.tsx | 161 +++++++++++------- .../src/repos/sources/modules/sources.ts | 91 +++++++--- 3 files changed, 165 insertions(+), 88 deletions(-) diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index dce6947622..99e3bde7ac 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -18,6 +18,7 @@ export type File = { subRepository?: SubRepository; // TODO partialResult: boolean; computationAborted: boolean; + truncated: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 0c5568d09c..55a9abdf18 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -7,28 +7,40 @@ import styled from "styled-components"; import { binder } from "@scm-manager/ui-extensions"; import { File, Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components"; -import { fetchSources, getFetchSourcesFailure, getSources, isFetchSourcesPending } from "../modules/sources"; +import { + fetchSources, + getFetchSourcesFailure, + getHunkCount, + getSources, + isFetchSourcesPending, isUpdateSourcePending +} from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; -import queryString from "query-string"; +import Button from "@scm-manager/ui-components/src/buttons/Button"; -type Props = WithTranslation & { +type Hunk = { + tree: File; loading: boolean; error: Error; - tree: File; + updateSources: (hunk: number) => void; +}; + +type Props = WithTranslation & { repository: Repository; revision: string; path: string; baseUrl: string; location: any; + hunks: Hunk[]; - updateSources: () => void; + // dispatch props + fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => void; // context props match: any; }; type State = { - stoppableUpdateHandler?: number; + stoppableUpdateHandler: number[]; }; const FixedWidthTh = styled.th` @@ -50,44 +62,57 @@ export function findParent(path: string) { class FileTree extends React.Component { constructor(props: Props) { super(props); - this.state = {}; + this.state = { stoppableUpdateHandler: [] }; } componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { - const { tree, updateSources } = this.props; - if (tree?._embedded?.children && tree._embedded.children.find(c => c.partialResult)) { - const stoppableUpdateHandler = setTimeout(updateSources, 3000); - this.setState({ stoppableUpdateHandler: stoppableUpdateHandler }); - } + const { hunks } = this.props; + hunks?.forEach((hunk, index) => { + if (hunk.tree?._embedded?.children && hunk.tree._embedded.children.find(c => c.partialResult)) { + const stoppableUpdateHandler = setTimeout(hunk.updateSources, 3000); + this.setState(prevState => { + return { + stoppableUpdateHandler: [...prevState.stoppableUpdateHandler, stoppableUpdateHandler] + }; + }); + } + }); } } componentWillUnmount(): void { - if (this.state.stoppableUpdateHandler) { - clearTimeout(this.state.stoppableUpdateHandler); - } + this.state.stoppableUpdateHandler.forEach(handler => clearTimeout(handler)); } + loadMore = () => { + // console.log("smth"); + }; + render() { - const { error, loading, tree } = this.props; + const { hunks, t } = this.props; - if (error) { - return ; - } - - if (loading) { - return ; - } - if (!tree) { + if (!hunks || hunks.length === 0) { return null; } - return
{this.renderSourcesTable()}
; + if (hunks.some(hunk => hunk.error)) { + return hunk.error)[0]} />; + } + + const lastHunk = hunks[hunks.length - 1]; + + return ( +
+ {this.renderSourcesTable()} + {lastHunk.loading && } + {lastHunk.tree?.truncated &&
+ ); } renderSourcesTable() { - const { tree, revision, path, baseUrl, t, location } = this.props; + const { hunks, revision, path, baseUrl, t } = this.props; const files = []; @@ -115,46 +140,41 @@ class FileTree extends React.Component { } }; - if (tree._embedded && tree._embedded.children) { - const children = [...tree._embedded.children].sort(compareFiles); - files.push(...children); - } + hunks + .filter(hunk => !hunk.loading) + .forEach(hunk => { + if (hunk.tree?._embedded && hunk.tree._embedded.children) { + const children = [...hunk.tree._embedded.children]; + files.push(...children); + } + }); if (files && files.length > 0) { - let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); } else { - baseUrlWithRevision += "/" + encodeURIComponent(tree.revision); - } - - const offset = queryString.parse(location.search).offset; - if (offset) { - baseUrlWithRevision += "?offset=" + offset; + baseUrlWithRevision += "/" + encodeURIComponent(hunks[0].tree.revision); } return ( - <> - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map((file: any) => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
- {tree.truncated &&

TRUNCATED

} - + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
); } return {t("sources.noSources")}; @@ -164,24 +184,37 @@ class FileTree extends React.Component { const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const updateSources = () => dispatch(fetchSources(repository, revision, path, false)); - - return { updateSources }; + return { + updateSources: (hunk: number) => dispatch(fetchSources(repository, revision, path, false, hunk)), + fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => { + dispatch(fetchSources(repository, revision, path, true, hunk)); + } + }; }; const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path); - const error = getFetchSourcesFailure(state, repository, revision, path); - const tree = getSources(state, repository, revision, path); + const loading = isFetchSourcesPending(state, repository, revision, path, 0); + const error = getFetchSourcesFailure(state, repository, revision, path, 0); + const hunkCount = getHunkCount(state, repository, revision, path); + const hunks = []; + for (let i = 0; i < hunkCount; ++i) { + console.log(`getting data for hunk ${i}`); + const tree = getSources(state, repository, revision, path, i); + const loading = isFetchSourcesPending(state, repository, revision, path, i); + hunks.push({ + tree, + loading + }); + } return { revision, path, loading, error, - tree + hunks }; }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 2c7413f7ea..51c5eeb0e1 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -10,34 +10,36 @@ export const FETCH_UPDATES_PENDING = `${FETCH_SOURCES}_UPDATE_PENDING`; export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`; export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`; -export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true) { +export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true, hunk = 0) { return function(dispatch: any, getState: () => any) { const state = getState(); if ( - isFetchSourcesPending(state, repository, revision, path) || - isUpdateSourcePending(state, repository, revision, path) + isFetchSourcesPending(state, repository, revision, path, hunk) || + isUpdateSourcePending(state, repository, revision, path, hunk) ) { return; } if (initialLoad) { - dispatch(fetchSourcesPending(repository, revision, path)); + dispatch(fetchSourcesPending(repository, revision, path, hunk)); } else { - dispatch(updateSourcesPending(repository, revision, path, getSources(state, repository, revision, path))); + dispatch( + updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) + ); } return apiClient - .get(createUrl(repository, revision, path)) + .get(createUrl(repository, revision, path, hunk)) .then(response => response.json()) .then((sources: File) => { - dispatch(fetchSourcesSuccess(repository, revision, path, sources)); + dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); }) .catch(err => { - dispatch(fetchSourcesFailure(repository, revision, path, err)); + dispatch(fetchSourcesFailure(repository, revision, path, hunk, err)); }); }; } -function createUrl(repository: Repository, revision: string, path: string) { +function createUrl(repository: Repository, revision: string, path: string, hunk: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -45,13 +47,14 @@ function createUrl(repository: Repository, revision: string, path: string) { // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; } -export function fetchSourcesPending(repository: Repository, revision: string, path: string): Action { +export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path), + payload: { hunk, pending: true, sources: {} } }; } @@ -59,24 +62,37 @@ export function updateSourcesPending( repository: Repository, revision: string, path: string, + hunk: number, currentSources: any ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { updatePending: true, sources: currentSources }, + payload: { hunk, updatePending: true, sources: currentSources }, itemId: createItemId(repository, revision, path) }; } -export function fetchSourcesSuccess(repository: Repository, revision: string, path: string, sources: File) { +export function fetchSourcesSuccess( + repository: Repository, + revision: string, + path: string, + hunk: number, + sources: File +) { return { type: FETCH_SOURCES_SUCCESS, - payload: { updatePending: false, sources }, + payload: { hunk, pending: false, updatePending: false, sources }, itemId: createItemId(repository, revision, path) }; } -export function fetchSourcesFailure(repository: Repository, revision: string, path: string, error: Error): Action { +export function fetchSourcesFailure( + repository: Repository, + revision: string, + path: string, + hunk: number, + error: Error +): Action { return { type: FETCH_SOURCES_FAILURE, payload: error, @@ -99,9 +115,14 @@ export default function reducer( } ): any { if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { + console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); return { ...state, - [action.itemId]: action.payload + [action.itemId + "/hunkCount"]: action.payload.hunk + 1, + [action.itemId + "/" + action.payload.hunk]: { + sources: action.payload.sources, + loading: false + } }; } return state; @@ -110,7 +131,7 @@ export default function reducer( // selectors export function isDirectory(state: any, repository: Repository, revision: string, path: string): boolean { - const currentFile = getSources(state, repository, revision, path); + const currentFile = getSources(state, repository, revision, path, 0); if (currentFile && !currentFile.directory) { return false; } else { @@ -118,31 +139,53 @@ export function isDirectory(state: any, repository: Repository, revision: string } } +export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { + if (state.sources) { + const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + return count ? count : 0; + } + return 0; +} + export function getSources( state: any, repository: Repository, revision: string | undefined, - path: string + path: string, + hunk: number ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path)]?.sources; + return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; } return null; } -export function isFetchSourcesPending(state: any, repository: Repository, revision: string, path: string): boolean { +export function isFetchSourcesPending( + state: any, + repository: Repository, + revision: string, + path: string, + hunk: number +): boolean { return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); } -function isUpdateSourcePending(state: any, repository: Repository, revision: string, path: string): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path)]?.updatePending; +export function isUpdateSourcePending( + state: any, + repository: Repository, + revision: string, + path: string, + hunk: number +): boolean { + return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; } export function getFetchSourcesFailure( state: any, repository: Repository, revision: string, - path: string + path: string, + hunk: number ): Error | null | undefined { return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); } From c4a801a7be919c80bdd900cf9a68c6a057a7625b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 20 Feb 2020 14:36:13 +0100 Subject: [PATCH 13/53] WIP --- .../repository/spi/BrowseCommandRequest.java | 10 ++- scm-ui/ui-types/src/Sources.ts | 6 +- .../src/repos/sources/components/FileTree.tsx | 26 ++---- .../src/repos/sources/modules/sources.test.ts | 84 ++++++++++++----- .../src/repos/sources/modules/sources.ts | 89 +++++++++++++------ 5 files changed, 137 insertions(+), 78 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 70dec0980a..4477a26e31 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -112,10 +112,11 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest final BrowseCommandRequest other = (BrowseCommandRequest) obj; - return super.equals(obj) && Objects.equal(recursive, other.recursive) + return super.equals(obj) + && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) - && Objects.equal(disableSubRepositoryDetection, - other.disableSubRepositoryDetection); + && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) + && Objects.equal(offset, other.offset); } /** @@ -128,7 +129,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection); + disableSubRepositoryDetection, offset); } /** @@ -147,6 +148,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest .add("recursive", recursive) .add("disableLastCommit", disableLastCommit) .add("disableSubRepositoryDetection", disableSubRepositoryDetection) + .add("offset", offset) .toString(); //J+ } diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 99e3bde7ac..b1cca3c59f 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -16,9 +16,9 @@ export type File = { length?: number; commitDate?: string; subRepository?: SubRepository; // TODO - partialResult: boolean; - computationAborted: boolean; - truncated: boolean; + partialResult?: boolean; + computationAborted?: boolean; + truncated?: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 55a9abdf18..00cf1de981 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -12,7 +12,7 @@ import { getFetchSourcesFailure, getHunkCount, getSources, - isFetchSourcesPending, isUpdateSourcePending + isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; import Button from "@scm-manager/ui-components/src/buttons/Button"; @@ -86,7 +86,7 @@ class FileTree extends React.Component { } loadMore = () => { - // console.log("smth"); + this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; render() { @@ -124,22 +124,6 @@ class FileTree extends React.Component { }); } - const compareFiles = function(f1: File, f2: File): number { - if (f1.directory) { - if (f2.directory) { - return f1.name.localeCompare(f2.name); - } else { - return -1; - } - } else { - if (f2.directory) { - return 1; - } else { - return f1.name.localeCompare(f2.name); - } - } - }; - hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -149,7 +133,9 @@ class FileTree extends React.Component { } }); - if (files && files.length > 0) { + const loading = hunks.filter(hunk => hunk.loading).length > 0; + + if (loading || (files && files.length > 0)) { let baseUrlWithRevision = baseUrl; if (revision) { baseUrlWithRevision += "/" + encodeURIComponent(revision); @@ -195,7 +181,6 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const mapStateToProps = (state: any, ownProps: Props) => { const { repository, revision, path } = ownProps; - const loading = isFetchSourcesPending(state, repository, revision, path, 0); const error = getFetchSourcesFailure(state, repository, revision, path, 0); const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; @@ -212,7 +197,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { return { revision, path, - loading, error, hunks }; diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fa6ff5c1f6..fdd7a9fc78 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -37,6 +37,9 @@ const collection = { length: 176, revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4", subRepository: undefined, + truncated: true, + partialResult: false, + computationAborted: false, _links: { self: { href: @@ -120,12 +123,23 @@ describe("sources fetch", () => { const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/_/" + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/_/", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/_//", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -136,17 +150,28 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository with the given revision and path", () => { - fetchMock.getOnce(sourcesUrl + "abc/src", collection); + fetchMock.getOnce(sourcesUrl + "abc/src?offset=0", collection); const expectedActions = [ { type: FETCH_SOURCES_PENDING, - itemId: "scm/core/abc/src" + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: true, + sources: {} + } }, { type: FETCH_SOURCES_SUCCESS, - itemId: "scm/core/abc/src", - payload: { updatePending: false, sources: collection } + itemId: "scm/core/abc/src/", + payload: { + hunk: 0, + updatePending: false, + pending: false, + sources: collection + } } ]; @@ -166,7 +191,7 @@ describe("sources fetch", () => { const actions = store.getActions(); expect(actions[0].type).toBe(FETCH_SOURCES_PENDING); expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE); - expect(actions[1].itemId).toBe("scm/core/_/"); + expect(actions[1].itemId).toBe("scm/core/_//"); expect(actions[1].payload).toBeDefined(); }); }); @@ -180,16 +205,18 @@ describe("reducer tests", () => { it("should store the collection, without revision and path", () => { const expectedState = { - "scm/core/_/": { updatePending: false, sources: collection } + "scm/core/_//0": { pending: false, updatePending: false, sources: collection }, + "scm/core/_//hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "", "", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "", "", 0, collection))).toEqual(expectedState); }); it("should store the collection, with revision and path", () => { const expectedState = { - "scm/core/abc/src/main": { updatePending: false, sources: collection } + "scm/core/abc/src/main/0": { pending: false, updatePending: false, sources: collection }, + "scm/core/abc/src/main/hunkCount": 1 }; - expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", collection))).toEqual(expectedState); + expect(reducer({}, fetchSourcesSuccess(repository, "abc", "src/main", 0, collection))).toEqual(expectedState); }); }); @@ -197,7 +224,7 @@ describe("selector tests", () => { it("should return false if it is no directory", () => { const state = { sources: { - "scm/core/abc/src/main/package.json": { + "scm/core/abc/src/main/package.json/0": { sources: { noDirectory } } } @@ -208,7 +235,7 @@ describe("selector tests", () => { it("should return true if it is directory", () => { const state = { sources: { - "scm/core/abc/src": noDirectory + "scm/core/abc/src/0": noDirectory } }; expect(isDirectory(state, repository, "abc", "src")).toBe(true); @@ -221,7 +248,7 @@ describe("selector tests", () => { it("should return the source collection without revision and path", () => { const state = { sources: { - "scm/core/_/": { + "scm/core/_//0": { sources: collection } } @@ -232,7 +259,7 @@ describe("selector tests", () => { it("should return the source collection with revision and path", () => { const state = { sources: { - "scm/core/abc/src/main": { + "scm/core/abc/src/main/0": { sources: collection } } @@ -242,15 +269,26 @@ describe("selector tests", () => { it("should return true, when fetch sources is pending", () => { const state = { - pending: { - [FETCH_SOURCES + "/scm/core/_/"]: true + sources: { + "scm/core/_//0": { + pending: true, + sources: {} + } } }; - expect(isFetchSourcesPending(state, repository, "", "")).toEqual(true); + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(true); }); it("should return false, when fetch sources is not pending", () => { - expect(isFetchSourcesPending({}, repository, "", "")).toEqual(false); + const state = { + sources: { + "scm/core/_//0": { + pending: false, + sources: {} + } + } + }; + expect(isFetchSourcesPending(state, repository, "", "", 0)).toEqual(false); }); const error = new Error("incredible error from hell"); @@ -258,13 +296,13 @@ describe("selector tests", () => { it("should return error when fetch sources did fail", () => { const state = { failure: { - [FETCH_SOURCES + "/scm/core/_/"]: error + [FETCH_SOURCES + "/scm/core/_//"]: error } }; - expect(getFetchSourcesFailure(state, repository, "", "")).toEqual(error); + expect(getFetchSourcesFailure(state, repository, "", "", 0)).toEqual(error); }); it("should return undefined when fetch sources did not fail", () => { - expect(getFetchSourcesFailure({}, repository, "", "")).toBe(undefined); + expect(getFetchSourcesFailure({}, repository, "", "", 0)).toBe(undefined); }); }); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 51c5eeb0e1..a12bf3ba0d 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -1,7 +1,6 @@ import * as types from "../../../modules/types"; import { Action, File, Link, Repository } from "@scm-manager/ui-types"; import { apiClient } from "@scm-manager/ui-components"; -import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES"; @@ -27,8 +26,18 @@ export function fetchSources(repository: Repository, revision: string, path: str updateSourcesPending(repository, revision, path, hunk, getSources(state, repository, revision, path, hunk)) ); } + + let offset = 0; + const hunkCount = getHunkCount(state, repository, revision, path); + for (let i = 0; i < hunkCount; ++i) { + const sources = getSources(state, repository, revision, path, i); + if (sources?._embedded.children) { + offset += sources._embedded.children.length; + } + } + return apiClient - .get(createUrl(repository, revision, path, hunk)) + .get(createUrl(repository, revision, path, offset)) .then(response => response.json()) .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); @@ -39,7 +48,7 @@ export function fetchSources(repository: Repository, revision: string, path: str }; } -function createUrl(repository: Repository, revision: string, path: string, hunk: number) { +function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { return base; @@ -47,14 +56,14 @@ function createUrl(repository: Repository, revision: string, path: string, hunk: // TODO handle trailing slash const pathDefined = path ? path : ""; - return `${base}${encodeURIComponent(revision)}/${pathDefined}?hunk=${hunk}`; + return `${base}${encodeURIComponent(revision)}/${pathDefined}?offset=${offset}`; } export function fetchSourcesPending(repository: Repository, revision: string, path: string, hunk: number): Action { return { type: FETCH_SOURCES_PENDING, - itemId: createItemId(repository, revision, path), - payload: { hunk, pending: true, sources: {} } + itemId: createItemId(repository, revision, path, ""), + payload: { hunk, pending: true, updatePending: false, sources: {} } }; } @@ -63,12 +72,12 @@ export function updateSourcesPending( revision: string, path: string, hunk: number, - currentSources: any + currentSources: File ): Action { return { type: FETCH_UPDATES_PENDING, - payload: { hunk, updatePending: true, sources: currentSources }, - itemId: createItemId(repository, revision, path) + payload: { hunk, pending: false, updatePending: true, sources: currentSources }, + itemId: createItemId(repository, revision, path, "") }; } @@ -82,7 +91,7 @@ export function fetchSourcesSuccess( return { type: FETCH_SOURCES_SUCCESS, payload: { hunk, pending: false, updatePending: false, sources }, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } @@ -96,14 +105,14 @@ export function fetchSourcesFailure( return { type: FETCH_SOURCES_FAILURE, payload: error, - itemId: createItemId(repository, revision, path) + itemId: createItemId(repository, revision, path, "") }; } -function createItemId(repository: Repository, revision: string | undefined, path: string) { +function createItemId(repository: Repository, revision: string | undefined, path: string, hunk: number | string) { const revPart = revision ? revision : "_"; const pathPart = path ? path : ""; - return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}`; + return `${repository.namespace}/${repository.name}/${decodeURIComponent(revPart)}/${pathPart}/${hunk}`; } // reducer @@ -114,18 +123,38 @@ export default function reducer( type: "UNKNOWN" } ): any { - if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) { - console.log("adding payload to " + action.itemId + "/" + action.payload.hunk); + if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) { return { ...state, - [action.itemId + "/hunkCount"]: action.payload.hunk + 1, - [action.itemId + "/" + action.payload.hunk]: { + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { sources: action.payload.sources, - loading: false + updatePending: false, + pending: false } }; + } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + sources: action.payload.sources, + updatePending: true, + pending: false + } + }; + } else if (action.itemId && action.type === FETCH_SOURCES_PENDING) { + return { + ...state, + [action.itemId + "hunkCount"]: action.payload.hunk + 1, + [action.itemId + action.payload.hunk]: { + updatePending: false, + pending: true + } + }; + } else { + return state; } - return state; } // selectors @@ -141,7 +170,7 @@ export function isDirectory(state: any, repository: Repository, revision: string export function getHunkCount(state: any, repository: Repository, revision: string | undefined, path: string): number { if (state.sources) { - const count = state.sources[createItemId(repository, revision, path) + "/hunkCount"]; + const count = state.sources[createItemId(repository, revision, path, "hunkCount")]; return count ? count : 0; } return 0; @@ -152,10 +181,10 @@ export function getSources( repository: Repository, revision: string | undefined, path: string, - hunk: number + hunk = 0 ): File | null | undefined { if (state.sources) { - return state.sources[createItemId(repository, revision, path) + "/" + hunk]?.sources; + return state.sources[createItemId(repository, revision, path, hunk)]?.sources; } return null; } @@ -165,9 +194,12 @@ export function isFetchSourcesPending( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): boolean { - return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path)); + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.pending; + } + return false; } export function isUpdateSourcePending( @@ -177,7 +209,10 @@ export function isUpdateSourcePending( path: string, hunk: number ): boolean { - return state?.sources && state.sources[createItemId(repository, revision, path) + "/" + hunk]?.updatePending; + if (state.sources) { + return state.sources[createItemId(repository, revision, path, hunk)]?.updatePending; + } + return false; } export function getFetchSourcesFailure( @@ -185,7 +220,7 @@ export function getFetchSourcesFailure( repository: Repository, revision: string, path: string, - hunk: number + hunk = 0 ): Error | null | undefined { - return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path)); + return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path, "")); } From 264f6efd049ab8e25a7c3566cf398576441efb1b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 07:37:55 +0100 Subject: [PATCH 14/53] Fix loading detection --- .../src/repos/sources/components/FileTree.tsx | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 00cf1de981..f3ecba8982 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -105,7 +105,6 @@ class FileTree extends React.Component { return (
{this.renderSourcesTable()} - {lastHunk.loading && } {lastHunk.tree?.truncated &&
); @@ -124,6 +123,10 @@ class FileTree extends React.Component { }); } + if (hunks.every(hunk => hunk.loading)) { + return ; + } + hunks .filter(hunk => !hunk.loading) .forEach(hunk => { @@ -144,23 +147,26 @@ class FileTree extends React.Component { } return ( - - - - - - - - - {binder.hasExtension("repos.sources.tree.row.right") && - - - {files.map((file: any) => ( - - ))} - -
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} -
+ <> + + + + + + + + + {binder.hasExtension("repos.sources.tree.row.right") && + + + {files.map((file: any) => ( + + ))} + +
{t("sources.file-tree.name")}{t("sources.file-tree.length")}{t("sources.file-tree.commitDate")}{t("sources.file-tree.description")}} +
+ {hunks[hunks.length - 1].loading && } + ); } return {t("sources.noSources")}; @@ -185,7 +191,6 @@ const mapStateToProps = (state: any, ownProps: Props) => { const hunkCount = getHunkCount(state, repository, revision, path); const hunks = []; for (let i = 0; i < hunkCount; ++i) { - console.log(`getting data for hunk ${i}`); const tree = getSources(state, repository, revision, path, i); const loading = isFetchSourcesPending(state, repository, revision, path, i); hunks.push({ From 722e38788b0fbd39d63525d030bdbb24a6f05770 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 09:45:43 +0100 Subject: [PATCH 15/53] Fix update --- .../ui-webapp/src/repos/sources/components/FileTree.tsx | 8 ++++---- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index f3ecba8982..881a841a57 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -21,7 +21,6 @@ type Hunk = { tree: File; loading: boolean; error: Error; - updateSources: (hunk: number) => void; }; type Props = WithTranslation & { @@ -34,6 +33,7 @@ type Props = WithTranslation & { // dispatch props fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => void; + updateSources: (hunk: number) => () => void; // context props match: any; @@ -67,10 +67,10 @@ class FileTree extends React.Component { componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevState.stoppableUpdateHandler === this.state.stoppableUpdateHandler) { - const { hunks } = this.props; + const { hunks, updateSources } = this.props; hunks?.forEach((hunk, index) => { if (hunk.tree?._embedded?.children && hunk.tree._embedded.children.find(c => c.partialResult)) { - const stoppableUpdateHandler = setTimeout(hunk.updateSources, 3000); + const stoppableUpdateHandler = setTimeout(updateSources(index), 3000); this.setState(prevState => { return { stoppableUpdateHandler: [...prevState.stoppableUpdateHandler, stoppableUpdateHandler] @@ -177,7 +177,7 @@ const mapDispatchToProps = (dispatch: any, ownProps: Props) => { const { repository, revision, path } = ownProps; return { - updateSources: (hunk: number) => dispatch(fetchSources(repository, revision, path, false, hunk)), + updateSources: (hunk: number) => () => dispatch(fetchSources(repository, revision, path, false, hunk)), fetchSources: (repository: Repository, revision: string, path: string, hunk: number) => { dispatch(fetchSources(repository, revision, path, true, hunk)); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index a12bf3ba0d..d37833c1c3 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -28,8 +28,7 @@ export function fetchSources(repository: Repository, revision: string, path: str } let offset = 0; - const hunkCount = getHunkCount(state, repository, revision, path); - for (let i = 0; i < hunkCount; ++i) { + for (let i = 0; i < hunk; ++i) { const sources = getSources(state, repository, revision, path, i); if (sources?._embedded.children) { offset += sources._embedded.children.length; @@ -136,7 +135,6 @@ export default function reducer( } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { return { ...state, - [action.itemId + "hunkCount"]: action.payload.hunk + 1, [action.itemId + action.payload.hunk]: { sources: action.payload.sources, updatePending: true, From a05c371910a531a78380a63658379c22458daf0a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 11:06:21 +0100 Subject: [PATCH 16/53] Render truncated info --- scm-ui/ui-webapp/public/locales/de/repos.json | 4 ++- scm-ui/ui-webapp/public/locales/en/repos.json | 4 ++- .../src/repos/sources/components/FileTree.tsx | 25 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 27909f338c..2b7b0f2ca4 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -128,7 +128,9 @@ "noSources": "Keine Sources in diesem Branch gefunden.", "extension": { "notBound": "Keine Erweiterung angebunden." - } + }, + "loadMore": "Laden", + "moreEntriesAvailable": "Es werden nur die ersten {{count}} Einträge angezeigt. Es sind weitere Einträge vorhanden." }, "permission": { "title": "Berechtigungen bearbeiten", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 793c01388d..d5a5a7fff6 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -128,7 +128,9 @@ "noSources": "No sources found for this branch.", "extension": { "notBound": "No extension bound." - } + }, + "loadMore": "Load", + "moreEntriesAvailable": "These are just the first {{count}} entries. There are more entries available." }, "permission": { "title": "Edit Permissions", diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 881a841a57..df70b44500 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -89,8 +89,27 @@ class FileTree extends React.Component { this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length); }; - render() { + renderTruncatedInfo = () => { const { hunks, t } = this.props; + const lastHunk = hunks[hunks.length - 1]; + const entryCount = hunks + .filter(hunk => hunk?.tree?._embedded?.children) + .map(hunk => hunk.tree._embedded.children.length) + .reduce((a, b) => a + b, 0); + if (lastHunk.tree?.truncated) { + return ( + +
+
{t("sources.moreEntriesAvailable", { count: entryCount })}
+
+
+ ); + } + }; + + render() { + const { hunks } = this.props; if (!hunks || hunks.length === 0) { return null; @@ -100,12 +119,10 @@ class FileTree extends React.Component { return hunk.error)[0]} />; } - const lastHunk = hunks[hunks.length - 1]; - return (
{this.renderSourcesTable()} - {lastHunk.tree?.truncated &&
); } From a8694eb668348738f659a8fbc090bdb07b051e70 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 11:50:04 +0100 Subject: [PATCH 17/53] Fix offset for svn repositories --- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index d37833c1c3..857229f8c2 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -50,7 +50,7 @@ export function fetchSources(repository: Repository, revision: string, path: str function createUrl(repository: Repository, revision: string, path: string, offset: number) { const base = (repository._links.sources as Link).href; if (!revision && !path) { - return base; + return `${base}?offset=${offset}`; } // TODO handle trailing slash From 4a82c541b293e198c602e598297f9d21efab19a9 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 12:49:43 +0100 Subject: [PATCH 18/53] Sort svn files --- .../scm/repository/spi/SvnBrowseCommand.java | 6 ++++- .../repository/spi/SvnBrowseCommandTest.java | 27 +++++++------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 1daaf5af61..a6e131fa4a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -51,9 +51,12 @@ import sonia.scm.repository.SubRepository; import sonia.scm.repository.SvnUtil; import sonia.scm.util.Util; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; +import static java.util.Comparator.comparing; import static org.tmatesoft.svn.core.SVNErrorCode.FS_NO_SUCH_REVISION; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -130,7 +133,8 @@ public class SvnBrowseCommand extends AbstractSvnCommand FileObject parent, String basePath) throws SVNException { - Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null); + List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); + entries.sort(comparing(SVNDirEntry::getName)); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 589f0d4107..0fc39c8174 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -39,6 +39,7 @@ import sonia.scm.repository.FileObject; import java.io.IOException; import java.util.Collection; +import java.util.Iterator; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -77,8 +78,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = foList1; - FileObject a = getFileObject(foList, "a.txt"); - FileObject c = getFileObject(foList, "c"); + Iterator iterator = foList.iterator(); + FileObject a = iterator.next(); + FileObject c = iterator.next(); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); @@ -113,20 +115,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertFalse(foList.isEmpty()); assertEquals(2, foList.size()); - FileObject d = null; - FileObject e = null; - - for (FileObject f : foList) - { - if ("d.txt".equals(f.getName())) - { - d = f; - } - else if ("e.txt".equals(f.getName())) - { - e = f; - } - } + Iterator iterator = foList.iterator(); + FileObject d = iterator.next(); + FileObject e = iterator.next(); assertNotNull(d); assertFalse(d.isDirectory()); @@ -198,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt"); + assertThat(foList).extracting("name").containsExactly("a.txt"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -212,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c"); + assertThat(foList).extracting("name").containsExactly("c"); } /** From 736ea3d93f2b0097d57e431231bf184216be991e Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 14:29:09 +0100 Subject: [PATCH 19/53] Sort git files --- .../scm/repository/spi/BrowseCommand.java | 15 +++++ .../java/sonia/scm/repository/GitUtil.java | 6 +- .../scm/repository/spi/GitBrowseCommand.java | 66 +++++++++++++++---- .../scm/repository/spi/SvnBrowseCommand.java | 2 +- .../repository/spi/SvnBrowseCommandTest.java | 6 +- 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index ee37d6243e..7859c1845b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -38,6 +38,9 @@ package sonia.scm.repository.spi; import sonia.scm.repository.BrowserResult; import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; //~--- JDK imports ------------------------------------------------------------ @@ -60,4 +63,16 @@ public interface BrowseCommand * @throws IOException */ BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException; + + default void sort(List entries, Function isDirectory, Function nameOf) { + entries.sort((e1, e2) -> { + if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) { + return nameOf.apply(e1).toLowerCase(Locale.ENGLISH).compareTo(nameOf.apply(e2).toLowerCase(Locale.ENGLISH)); + } else if (isDirectory.apply(e1)) { + return -1; + } else { + return 1; + } + }); + } } 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 a93c1b5d81..e816aaf76d 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 @@ -749,9 +749,13 @@ public final class GitUtil } public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + return getLfsPointer(repo, blobId, attributes); + } + + public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, ObjectId blobId, Attributes attributes) throws IOException { 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)); } 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 f53b7ec0a4..fe75e6d1b1 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 @@ -69,7 +69,9 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -158,14 +160,14 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, - BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) + BrowseCommandRequest request, ObjectId revId, TreeEntry treeEntry) throws IOException { FileObject file = new FileObject(); - String path = treeWalk.getPathString(); + String path = treeEntry.getPathString(); - file.setName(treeWalk.getNameString()); + file.setName(treeEntry.getNameString()); file.setPath(path); SubRepository sub = null; @@ -183,7 +185,7 @@ public class GitBrowseCommand extends AbstractGitCommand } else { - ObjectLoader loader = repo.open(treeWalk.getObjectId(0)); + ObjectLoader loader = repo.open(treeEntry.getObjectId()); file.setDirectory(loader.getType() == Constants.OBJ_TREE); @@ -195,7 +197,7 @@ public class GitBrowseCommand extends AbstractGitCommand try (RevWalk walk = new RevWalk(repo)) { commit = walk.parseCommit(revId); } - Optional lfsPointer = getLfsPointer(repo, path, commit, treeWalk); + Optional lfsPointer = getLfsPointer(repo, path, commit, treeEntry); if (lfsPointer.isPresent()) { setFileLengthFromLfsBlob(lfsPointer.get(), file); @@ -253,11 +255,18 @@ public class GitBrowseCommand extends AbstractGitCommand } private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { - List files = Lists.newArrayList(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) - { + List entries = new ArrayList<>(); + while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { + entries.add(new TreeEntry(repo, treeWalk)); + } + sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); - FileObject fileObject = createFileObject(repo, request, revId, treeWalk); + List files = Lists.newArrayList(); + Iterator entryIterator = entries.iterator(); + while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) + { + TreeEntry entry = entryIterator.next(); + FileObject fileObject = createFileObject(repo, request, revId, entry); if (!fileObject.getPath().startsWith(parent.getPath())) { parent.setChildren(files); return fileObject; @@ -298,7 +307,7 @@ public class GitBrowseCommand extends AbstractGitCommand currentDepth++; if (currentDepth >= limit) { - return createFileObject(repo, request, revId, treeWalk); + return createFileObject(repo, request, revId, new TreeEntry(repo, treeWalk)); } else { treeWalk.enterSubtree(); } @@ -338,11 +347,11 @@ public class GitBrowseCommand extends AbstractGitCommand return null; } - private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) { + private Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeEntry treeWalk) { try { Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit); - return GitUtil.getLfsPointer(repo, treeWalk, attributes); + return GitUtil.getLfsPointer(repo, treeWalk.getObjectId(), attributes); } catch (IOException e) { throw new InternalRepositoryException(repository, "could not read lfs pointer", e); } @@ -448,4 +457,37 @@ public class GitBrowseCommand extends AbstractGitCommand return changed; } } + + private static class TreeEntry { + + private final String pathString; + private final String nameString; + private final ObjectId objectId; + private final boolean directory; + + public TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { + this.pathString = treeWalk.getPathString(); + this.nameString = treeWalk.getNameString(); + this.objectId = treeWalk.getObjectId(0); + ObjectLoader loader = repo.open(objectId); + + this.directory = loader.getType() == Constants.OBJ_TREE; + } + + public String getPathString() { + return pathString; + } + + public String getNameString() { + return nameString; + } + + public ObjectId getObjectId() { + return objectId; + } + + public boolean isDirectory() { + return directory; + } + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index a6e131fa4a..423b82004a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -134,7 +134,7 @@ public class SvnBrowseCommand extends AbstractSvnCommand throws SVNException { List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); - entries.sort(comparing(SVNDirEntry::getName)); + sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 0fc39c8174..1c6e228a6d 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -79,8 +79,8 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = foList1; Iterator iterator = foList.iterator(); - FileObject a = iterator.next(); FileObject c = iterator.next(); + FileObject a = iterator.next(); assertFalse(a.isDirectory()); assertEquals("a.txt", a.getName()); @@ -189,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("a.txt"); + assertThat(foList).extracting("name").containsExactly("c"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -203,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("c"); + assertThat(foList).extracting("name").containsExactly("a.txt"); } /** From f0da22ad296a6b3d67c354e4a6acab0ab0db0ce8 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 14:50:07 +0100 Subject: [PATCH 20/53] Fix git sort --- .../main/java/sonia/scm/repository/spi/GitBrowseCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fe75e6d1b1..8d25504b86 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 @@ -256,7 +256,7 @@ public class GitBrowseCommand extends AbstractGitCommand private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { List entries = new ArrayList<>(); - while (treeWalk.next() && ++resultCount <= request.getLimit() + request.getOffset()) { + while (treeWalk.next()) { entries.add(new TreeEntry(repo, treeWalk)); } sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); From 58625ba606dfd51ac957a34bd1cb57edf3ebaac6 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 21 Feb 2020 15:00:44 +0100 Subject: [PATCH 21/53] Sort files with an upper case letter first --- .../scm/repository/spi/BrowseCommand.java | 2 +- .../scm/repository/spi/BrowseCommandTest.java | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java index 7859c1845b..899bda077b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java @@ -67,7 +67,7 @@ public interface BrowseCommand default void sort(List entries, Function isDirectory, Function nameOf) { entries.sort((e1, e2) -> { if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) { - return nameOf.apply(e1).toLowerCase(Locale.ENGLISH).compareTo(nameOf.apply(e2).toLowerCase(Locale.ENGLISH)); + return nameOf.apply(e1).compareTo(nameOf.apply(e2)); } else if (isDirectory.apply(e1)) { return -1; } else { diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java new file mode 100644 index 0000000000..1ed39c8366 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java @@ -0,0 +1,70 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.BrowserResult; + +import java.io.IOException; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.d; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.f; + +class BrowseCommandTest implements BrowseCommand { + + @Test + void shouldSort() { + List entries = asList( + f("b.txt"), + f("a.txt"), + f("Dockerfile"), + f(".gitignore"), + d("src"), + f("README") + ); + + sort(entries, Entry::isDirecotry, Entry::getName); + + assertThat(entries).extracting("name") + .containsExactly( + "src", + ".gitignore", + "Dockerfile", + "README", + "a.txt", + "b.txt" + ); + } + + @Override + public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { + return null; + } + + static class Entry { + private final String name; + private final boolean direcotry; + + static Entry f(String name) { + return new Entry(name, false); + } + + static Entry d(String name) { + return new Entry(name, true); + } + + public Entry(String name, boolean direcotry) { + this.name = name; + this.direcotry = direcotry; + } + + public String getName() { + return name; + } + + public boolean isDirecotry() { + return direcotry; + } + } +} From 76665b4dbe5b9105edb39cd6a8855e365210d7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 25 Feb 2020 08:25:25 +0100 Subject: [PATCH 22/53] Sort hg files --- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 3 ++- .../java/sonia/scm/repository/spi/HgBrowseCommandTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 86e6175d55..51e33349cf 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -153,7 +153,8 @@ class File_Walker: return path def walk(self, structure, parent = ""): - for key, value in structure.iteritems(): + sortedItems = sorted(structure.iteritems(), key = lambda item: item[1]) + for key, value in sortedItems: if key == FILE_MARKER: if value: for v in value: diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index f74d036843..bf34a0ad92 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -190,7 +190,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("a.txt", "b.txt"); + assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "a.txt"); assertThat(root.isTruncated()).isTrue(); } @@ -205,7 +205,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "f.txt"); + assertThat(foList).extracting("name").containsExactlyInAnyOrder("b.txt", "f.txt"); assertThat(root.isTruncated()).isFalse(); } From c0e0ed3d17eab175bbf658072f3c89428883f312 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 25 Feb 2020 17:34:41 +0100 Subject: [PATCH 23/53] Fix tests --- .../sonia/scm/repository/spi/GitBrowseCommandTest.java | 8 ++++---- .../ui-webapp/src/repos/sources/modules/sources.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) 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 ce8214e2fa..23d3bfabca 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 @@ -87,7 +87,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList) .extracting("name") - .containsExactly("a.txt", "b.txt", "c", "f.txt"); + .containsExactly("c", "a.txt", "b.txt", "f.txt"); } @Test @@ -100,7 +100,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); assertThat(foList) .extracting("name") - .containsExactly("a.txt", "c"); + .containsExactly("c", "a.txt"); } @Test @@ -207,7 +207,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertThat(foList) .extracting("name") - .containsExactly("a.txt", "b.txt", "c", "f.txt"); + .containsExactly("c", "a.txt", "b.txt", "f.txt"); FileObject c = findFile(foList, "c"); @@ -262,7 +262,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").contains("c", "f.txt"); + assertThat(foList).extracting("name").contains("b.txt", "f.txt"); assertFalse(root.isTruncated()); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index fdd7a9fc78..f49488a7df 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -118,7 +118,7 @@ describe("sources fetch", () => { }); it("should fetch the sources of the repository", () => { - fetchMock.getOnce(sourcesUrl, collection); + fetchMock.getOnce(sourcesUrl + "?offset=0", collection); const expectedActions = [ { @@ -182,7 +182,7 @@ describe("sources fetch", () => { }); it("should dispatch FETCH_SOURCES_FAILURE on server error", () => { - fetchMock.getOnce(sourcesUrl, { + fetchMock.getOnce(sourcesUrl + "?offset=0", { status: 500 }); From 67192a203e7500854690f874a424e7188ea75430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 10:54:16 +0100 Subject: [PATCH 24/53] Read and sort tree first before applying limit --- .../scm/repository/spi/GitBrowseCommand.java | 73 +++++++++++++------ .../repository/spi/GitBrowseCommandTest.java | 4 +- 2 files changed, 54 insertions(+), 23 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 8d25504b86..5bd25e549b 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 @@ -77,6 +77,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import static java.util.Collections.emptyList; import static java.util.Optional.empty; import static java.util.Optional.of; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -254,34 +255,26 @@ public class GitBrowseCommand extends AbstractGitCommand return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath()); } - private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { - List entries = new ArrayList<>(); - while (treeWalk.next()) { - entries.add(new TreeEntry(repo, treeWalk)); - } - sort(entries, TreeEntry::isDirectory, TreeEntry::getNameString); + private void findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException { + TreeEntry entry = new TreeEntry(); + createTree(parent.getPath(), entry, repo, request, treeWalk); + convertToFileObject(parent, repo, request, revId, entry.getChildren()); + } + private void convertToFileObject(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, List entries) throws IOException { List files = Lists.newArrayList(); Iterator entryIterator = entries.iterator(); while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) { TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); - if (!fileObject.getPath().startsWith(parent.getPath())) { - parent.setChildren(files); - return fileObject; - } if (resultCount > request.getOffset()) { files.add(fileObject); } if (request.isRecursive() && fileObject.isDirectory()) { - treeWalk.enterSubtree(); - FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk); - if (rc != null) { - files.add(rc); - } + convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } } @@ -290,8 +283,27 @@ public class GitBrowseCommand extends AbstractGitCommand if (resultCount > request.getLimit() + request.getOffset()) { parent.setTruncated(true); } + } - return null; + private Optional createTree(String path, TreeEntry parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, TreeWalk treeWalk) throws IOException { + List entries = new ArrayList<>(); + while (treeWalk.next()) { + TreeEntry treeEntry = new TreeEntry(repo, treeWalk); + if (!treeEntry.getPathString().startsWith(path)) { + parent.setChildren(entries); + return of(treeEntry); + } + + entries.add(treeEntry); + + if (request.isRecursive() && treeEntry.isDirectory()) { + treeWalk.enterSubtree(); + Optional surplus = createTree(treeEntry.getNameString(), treeEntry, repo, request, treeWalk); + surplus.ifPresent(entries::add); + } + } + parent.setChildren(entries); + return empty(); } private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, @@ -458,14 +470,22 @@ public class GitBrowseCommand extends AbstractGitCommand } } - private static class TreeEntry { + private class TreeEntry { private final String pathString; private final String nameString; private final ObjectId objectId; private final boolean directory; + private List children = emptyList(); - public TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { + TreeEntry() { + pathString = ""; + nameString = ""; + objectId = null; + directory = true; + } + + TreeEntry(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk) throws IOException { this.pathString = treeWalk.getPathString(); this.nameString = treeWalk.getNameString(); this.objectId = treeWalk.getObjectId(0); @@ -474,20 +494,29 @@ public class GitBrowseCommand extends AbstractGitCommand this.directory = loader.getType() == Constants.OBJ_TREE; } - public String getPathString() { + String getPathString() { return pathString; } - public String getNameString() { + String getNameString() { return nameString; } - public ObjectId getObjectId() { + ObjectId getObjectId() { return objectId; } - public boolean isDirectory() { + boolean isDirectory() { return directory; } + + List getChildren() { + return children; + } + + void setChildren(List children) { + sort(children, TreeEntry::isDirectory, TreeEntry::getNameString); + this.children = children; + } } } 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 23d3bfabca..53a5bb0296 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 @@ -175,7 +175,9 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).hasSize(2); + assertThat(foList) + .extracting("name") + .containsExactly("d.txt", "e.txt"); FileObject d = findFile(foList, "d.txt"); FileObject e = findFile(foList, "e.txt"); From 4e7381b98f0cc3d352731329088f591764172d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 11:10:01 +0100 Subject: [PATCH 25/53] Fix offset in recursion --- .../scm/repository/spi/GitBrowseCommand.java | 8 +-- .../repository/spi/GitBrowseCommandTest.java | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 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 5bd25e549b..f4b6f15eba 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 @@ -269,13 +269,13 @@ public class GitBrowseCommand extends AbstractGitCommand TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); - if (resultCount > request.getOffset()) { - files.add(fileObject); - } - if (request.isRecursive() && fileObject.isDirectory()) { convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } + + if (resultCount > request.getOffset()) { + files.add(fileObject); + } } parent.setChildren(files); 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 53a5bb0296..8684837d37 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 @@ -268,6 +268,75 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { assertFalse(root.isTruncated()); } + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + + FileObject c = findFile(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + private FileObject findFile(Collection foList, String name) { return foList.stream() .filter(f -> name.equals(f.getName())) From 77100888655635760c8b9f9bd38cb77c62e88d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 13:24:31 +0100 Subject: [PATCH 26/53] Fix offset in recursion --- .../scm/repository/spi/SvnBrowseCommand.java | 11 +-- .../repository/spi/SvnBrowseCommandTest.java | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 423b82004a..274155fd55 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -135,17 +135,18 @@ public class SvnBrowseCommand extends AbstractSvnCommand { List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); - for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ++resultCount) { + for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ) { + ++resultCount; SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); - if (resultCount >= request.getOffset()) { - parent.addChild(child); - } - if (child.isDirectory() && request.isRecursive()) { traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } + + if (resultCount > request.getOffset()) { + parent.addChild(child); + } } if (resultCount >= request.getLimit() + request.getOffset()) { parent.setTruncated(true); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 1c6e228a6d..f0bdb38c4a 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -206,6 +206,75 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertThat(foList).extracting("name").containsExactly("a.txt"); } + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = createCommand().getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + /** * Method description * From ffdd80df6168e2ae9cadef6942f04f9c74caaece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 15:59:03 +0100 Subject: [PATCH 27/53] Correct hash and equals for cache --- .../java/sonia/scm/repository/spi/BrowseCommandRequest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index 4477a26e31..c50ab06cee 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -116,7 +116,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest && Objects.equal(recursive, other.recursive) && Objects.equal(disableLastCommit, other.disableLastCommit) && Objects.equal(disableSubRepositoryDetection, other.disableSubRepositoryDetection) - && Objects.equal(offset, other.offset); + && Objects.equal(offset, other.offset) + && Objects.equal(limit, other.limit); } /** @@ -129,7 +130,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest public int hashCode() { return Objects.hashCode(super.hashCode(), recursive, disableLastCommit, - disableSubRepositoryDetection, offset); + disableSubRepositoryDetection, offset, limit); } /** From 8ff870ccb1411f4456c6d5ec20b2dd5fdccdb7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 16:05:49 +0100 Subject: [PATCH 28/53] Remove unused property --- .../scm/repository/spi/FileBaseCommandRequest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java index 0a2192897a..9f563345fd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/FileBaseCommandRequest.java @@ -147,10 +147,6 @@ public abstract class FileBaseCommandRequest this.revision = revision; } - public void setLimit(int limit) { - this.limit = limit; - } - //~--- get methods ---------------------------------------------------------- /** @@ -175,10 +171,6 @@ public abstract class FileBaseCommandRequest return revision; } - public int getLimit() { - return limit; - } - //~--- methods -------------------------------------------------------------- /** @@ -216,6 +208,4 @@ public abstract class FileBaseCommandRequest /** Field description */ private String revision; - - private int limit; } From 1567cd87653a33479da7c2500910b191b2d7088c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 26 Feb 2020 17:36:41 +0100 Subject: [PATCH 29/53] Add tests for recursive request --- .../repository/spi/HgBrowseCommandTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index bf34a0ad92..c017d0632c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -209,6 +209,76 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertThat(root.isTruncated()).isFalse(); } + + @Test + public void testRecursiveLimit() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(4); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + } + + @Test + public void testRecursiveLimitInSubDir() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setLimit(2); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("d.txt"); + } + + @Test + public void testRecursiveOffset() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + + request.setOffset(2); + request.setRecursive(true); + + FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); + + Collection foList = root.getChildren(); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + + FileObject c = getFileObject(foList, "c"); + + Collection cChildren = c.getChildren(); + assertThat(cChildren) + .extracting("name") + .containsExactly("e.txt"); + } + //~--- get methods ---------------------------------------------------------- /** From f74db39a4c66942500b073e44b8c6b6aa9d07b66 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 2 Mar 2020 13:28:45 +0100 Subject: [PATCH 30/53] fix unittests for file order --- .../sonia/scm/hg/ext/fileview_test.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py index 2ce3989d58..4400f70de5 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py @@ -45,19 +45,19 @@ class Test_File_Viewer(unittest.TestCase): def test_recursive(self): root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "", True) - self.assertChildren(root, ["a", "b", "f.txt", "c"]) - c = root[3] + self.assertChildren(root, ["c", "a", "b", "f.txt"]) + c = root[0] self.assertDirectory(c, "c") - self.assertChildren(c, ["c/d.txt", "c/e.txt", "c/g"]) - g = c[2] + self.assertChildren(c, ["c/g", "c/d.txt", "c/e.txt"]) + g = c[0] self.assertDirectory(g, "c/g") self.assertChildren(g, ["c/g/h.txt"]) def test_recursive_with_path(self): root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True) self.assertDirectory(root, "c") - self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/g"]) - g = root[2] + self.assertChildren(root, ["c/g", "c/d.txt", "c/e.txt"]) + g = root[0] self.assertDirectory(g, "c/g") self.assertChildren(g, ["c/g/h.txt"]) @@ -69,15 +69,15 @@ class Test_File_Viewer(unittest.TestCase): def test_non_recursive(self): root = self.collect(["a.txt", "b.txt", "c/d.txt", "c/e.txt", "c/f/g.txt"]) self.assertDirectory(root, "") - self.assertChildren(root, ["a.txt", "b.txt", "c"]) - c = root[2] + self.assertChildren(root, ["c", "a.txt", "b.txt"]) + c = root[0] self.assertEmptyDirectory(c, "c") def test_non_recursive_with_path(self): root = self.collect(["a.txt", "b.txt", "c/d.txt", "c/e.txt", "c/f/g.txt"], "c") self.assertDirectory(root, "c") - self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/f"]) - f = root[2] + self.assertChildren(root, ["c/f", "c/d.txt", "c/e.txt"]) + f = root[0] self.assertEmptyDirectory(f, "c/f") def test_non_recursive_with_path_with_ending_slash(self): @@ -96,7 +96,7 @@ class Test_File_Viewer(unittest.TestCase): viewer.sub_repositories = sub_repositories viewer.view() - d = collector[0][2] + d = collector[0][1] self.assertDirectory(d, "d") From b4441ae1c82187916082295c125036fffcc04e9d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 2 Mar 2020 14:48:17 +0100 Subject: [PATCH 31/53] make File_Printer testable --- .../resources/sonia/scm/hg/ext/fileview.py | 24 ++++--- .../sonia/scm/hg/ext/fileview_test.py | 66 ++++++++++++++++++- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 51e33349cf..b303b2f3d4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -196,10 +196,17 @@ def collect_sub_repositories(revCtx): return subrepos +class Writer: + def __init__(self, ui): + self.ui = ui + + def write(self, value): + self.ui.write(value) + class File_Printer: - def __init__(self, ui, repo, revCtx, disableLastCommit, transport, limit, offset): - self.ui = ui + def __init__(self, writer, repo, revCtx, disableLastCommit, transport, limit, offset): + self.writer = writer self.repo = repo self.revCtx = revCtx self.disableLastCommit = disableLastCommit @@ -213,7 +220,7 @@ class File_Printer: format = '%s/\n' if self.transport: format = 'd%s/\0' - self.ui.write( format % path) + self.writer.write( format % path) def print_file(self, path): if self.shouldPrintResult(): @@ -227,14 +234,14 @@ class File_Printer: format = '%s %i %s %s\n' if self.transport: format = 'f%s\n%i %s %s\0' - self.ui.write( format % (file.path(), file.size(), date, description) ) + self.writer.write( format % (file.path(), file.size(), date, description) ) def print_sub_repository(self, path, subrepo): if self.shouldPrintResult(): format = '%s/ %s %s\n' if self.transport: format = 's%s/\n%s %s\0' - self.ui.write( format % (path, subrepo.revision, subrepo.url)) + self.writer.write( format % (path, subrepo.revision, subrepo.url)) def visit(self, file): if file.sub_repository: @@ -256,9 +263,9 @@ class File_Printer: def finish(self): if self.isTruncated(): if self.transport: - self.ui.write( "t") + self.writer.write( "t") else: - self.ui.write("truncated") + self.writer.write("truncated") class File_Viewer: def __init__(self, revCtx, visitor): @@ -302,7 +309,8 @@ def fileview(ui, repo, **opts): subrepos = {} if not opts["disableSubRepositoryDetection"]: subrepos = collect_sub_repositories(revCtx) - printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["offset"]) + writer = Writer(ui) + printer = File_Printer(writer, repo, revCtx, opts["disableLastCommit"], opts["transport"], opts["limit"], opts["offset"]) viewer = File_Viewer(revCtx, printer) viewer.recursive = opts["recursive"] viewer.sub_repositories = subrepos diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py index 4400f70de5..13be49c5a9 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py @@ -1,11 +1,24 @@ -from fileview import File_Viewer, SubRepository +from fileview import File_Viewer, File_Printer, SubRepository import unittest +class DummyManifestEntry: + def __init__(self, name): + self.name = name + + def path(self): + return self.name + + def size(self): + return len(self.name) + class DummyRevContext(): def __init__(self, mf): self.mf = mf + def __getitem__(self, path): + return DummyManifestEntry(path) + def manifest(self): return self.mf @@ -31,6 +44,18 @@ class File_Object_Collector(): self.stack.append(file) self.last = file +class CollectingWriter: + def __init__(self): + self.stack = [] + + def __len__(self): + return len(self.stack) + + def __getitem__(self, key): + return self.stack[key] + + def write(self, value): + self.stack.append(value) class Test_File_Viewer(unittest.TestCase): @@ -53,6 +78,45 @@ class Test_File_Viewer(unittest.TestCase): self.assertDirectory(g, "c/g") self.assertChildren(g, ["c/g/h.txt"]) + def test_printer(self): + paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] + writer = self.view_with_limit_and_offset(paths, 1000, 0) + self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) + + def test_printer_with_limit(self): + paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] + writer = self.view_with_limit_and_offset(paths, 3, 0) + self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt"]) + + # TODO fix + def x_test_printer_with_offset(self): + paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] + writer = self.view_with_limit_and_offset(paths, 100, 3) + self.assertPaths(writer, ["/", "c", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) + + def view_with_limit_and_offset(self, paths, limit, offset): + revCtx = DummyRevContext(paths) + collector = File_Object_Collector() + + writer = CollectingWriter() + printer = File_Printer(writer, None, revCtx, True, False, limit, offset) + + viewer = File_Viewer(revCtx, printer) + viewer.recursive = True + viewer.view("") + return writer + + def assertPath(self, actual, expected): + path = actual[:len(expected)] + self.assertEqual(path, expected) + nextChar = actual[len(expected)] + self.assertTrue(nextChar == " " or nextChar == "\n", expected + " does not match " + actual) + + def assertPaths(self, actual, expected): + for idx,item in enumerate(actual): + self.assertPath(item, expected[idx]) + self.assertEqual(len(actual), len(expected)) + def test_recursive_with_path(self): root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True) self.assertDirectory(root, "c") From fa80c831099cac90b1f73654e850ce47d3500d0b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 3 Mar 2020 11:59:20 +0100 Subject: [PATCH 32/53] Implement new limit/offset rule for git Directories shall no longer be taken into account for limit/offset calculations. --- .../repository/api/BrowseCommandBuilder.java | 8 +++-- .../scm/repository/spi/GitBrowseCommand.java | 13 +++++--- .../repository/spi/GitBrowseCommandTest.java | 32 ++++++++++++++----- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index c312b20534..20c79e60dd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -306,7 +306,7 @@ public final class BrowseCommandBuilder * severe performance implications. Reading a repository with thousands of files in one folder * can generate a huge load for a longer time. * - * @param limit The maximal number of files this request shall return. + * @param limit The maximal number of files this request shall return (directories are not counted). * * @since 2.0.0 */ @@ -318,8 +318,10 @@ public final class BrowseCommandBuilder /** * Proceed the list from the given number on (zero based). * - * @param offset The number of the entry, the result should start with (zero based). - * All preceding entries will be omitted. + * @param offset The number of the file, the result should start with (zero based). + * All preceding files will be omitted. Directories are not + * counted. Therefore directories are only listed in results without + * offset. * @since 2.0.0 */ public BrowseCommandBuilder setOffset(int offset) { 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 f4b6f15eba..15086e89ba 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 @@ -264,25 +264,28 @@ public class GitBrowseCommand extends AbstractGitCommand private void convertToFileObject(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, List entries) throws IOException { List files = Lists.newArrayList(); Iterator entryIterator = entries.iterator(); - while (entryIterator.hasNext() && ++resultCount <= request.getLimit() + request.getOffset()) + boolean hasNext; + while ((hasNext = entryIterator.hasNext()) && resultCount < request.getLimit() + request.getOffset()) { TreeEntry entry = entryIterator.next(); FileObject fileObject = createFileObject(repo, request, revId, entry); + if (!fileObject.isDirectory()) { + ++resultCount; + } + if (request.isRecursive() && fileObject.isDirectory()) { convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } - if (resultCount > request.getOffset()) { + if (resultCount > request.getOffset() || fileObject.isDirectory()) { files.add(fileObject); } } parent.setChildren(files); - if (resultCount > request.getLimit() + request.getOffset()) { - parent.setTruncated(true); - } + parent.setTruncated(hasNext); } private Optional createTree(String path, TreeEntry parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, TreeWalk treeWalk) throws IOException { 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 8684837d37..48e7a6e9ff 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 @@ -242,21 +242,37 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { @Test public void testBrowseLimit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); FileObject root = createCommand() .getBrowserResult(request).getFile(); assertNotNull(root); Collection foList = root.getChildren(); + assertThat(foList).extracting("name").containsExactly("c", "a.txt"); assertThat(foList).hasSize(2); - assertTrue(root.isTruncated()); + assertTrue("result should be marked as trunctated", root.isTruncated()); + } + + @Test + public void testBrowseLimitWithoutTruncation() throws IOException { + BrowseCommandRequest request = new BrowseCommandRequest(); + request.setLimit(3); + FileObject root = createCommand() + .getBrowserResult(request).getFile(); + assertNotNull(root); + + Collection foList = root.getChildren(); + + assertThat(foList).extracting("name").containsExactly("c", "a.txt", "b.txt", "f.txt"); + assertThat(foList).hasSize(4); + assertFalse("result should not be marked as trunctated", root.isTruncated()); } @Test public void testBrowseOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); request.setOffset(2); FileObject root = createCommand() .getBrowserResult(request).getFile(); @@ -264,15 +280,15 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").contains("b.txt", "f.txt"); - assertFalse(root.isTruncated()); + assertThat(foList).extracting("name").contains("f.txt"); + assertFalse("result should not be marked as trunctated", root.isTruncated()); } @Test public void testRecursiveLimit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(4); + request.setLimit(3); request.setRecursive(true); FileObject root = createCommand().getBrowserResult(request).getFile(); @@ -295,7 +311,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { public void testRecursiveLimitInSubDir() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); request.setRecursive(true); FileObject root = createCommand().getBrowserResult(request).getFile(); @@ -318,7 +334,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { public void testRecursiveOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setOffset(2); + request.setOffset(1); request.setRecursive(true); FileObject root = createCommand().getBrowserResult(request).getFile(); From cf43e3bc84f2c6ccfc4b56bf4f8e6f8bbae6cf80 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Tue, 3 Mar 2020 17:06:55 +0100 Subject: [PATCH 33/53] Implement new limit/offset rule for git and svn Directories shall no longer be taken into account for limit/offset calculations. --- .../java/sonia/scm/repository/spi/GitBrowseCommand.java | 2 +- .../sonia/scm/repository/spi/GitBrowseCommandTest.java | 2 +- .../java/sonia/scm/repository/spi/SvnBrowseCommand.java | 7 +++++-- .../sonia/scm/repository/spi/SvnBrowseCommandTest.java | 8 ++++---- 4 files changed, 11 insertions(+), 8 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 15086e89ba..b6d6e7ed6a 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 @@ -278,7 +278,7 @@ public class GitBrowseCommand extends AbstractGitCommand convertToFileObject(fileObject, repo, request, revId, entry.getChildren()); } - if (resultCount > request.getOffset() || fileObject.isDirectory()) { + if (resultCount > request.getOffset() || (request.getOffset() == 0 && fileObject.isDirectory())) { files.add(fileObject); } } 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 48e7a6e9ff..0bef6d951b 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 @@ -280,7 +280,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").contains("f.txt"); + assertThat(foList).extracting("name").containsExactly("f.txt"); assertFalse("result should not be marked as trunctated", root.isTruncated()); } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java index 274155fd55..c362e628a4 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java @@ -136,15 +136,18 @@ public class SvnBrowseCommand extends AbstractSvnCommand List entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null)); sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName); for (Iterator iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ) { - ++resultCount; SVNDirEntry entry = iterator.next(); FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath); + if (!child.isDirectory()) { + ++resultCount; + } + if (child.isDirectory() && request.isRecursive()) { traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath())); } - if (resultCount > request.getOffset()) { + if (resultCount > request.getOffset() || (request.getOffset() == 0 && child.isDirectory())) { parent.addChild(child); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index f0bdb38c4a..6ddebf0bf9 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -189,7 +189,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("c"); + assertThat(foList).extracting("name").containsExactly("c", "a.txt"); assertThat(result.getFile().isTruncated()).isTrue(); } @@ -203,7 +203,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertThat(foList).extracting("name").containsExactly("a.txt"); + assertThat(foList).isEmpty(); } @Test @@ -233,7 +233,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase public void testRecursiveLimitInSubDir() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); request.setRecursive(true); FileObject root = createCommand().getBrowserResult(request).getFile(); @@ -256,7 +256,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase public void testRecursiveOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setOffset(2); + request.setOffset(1); request.setRecursive(true); FileObject root = createCommand().getBrowserResult(request).getFile(); From 5fb338fbd26fdf5ebeccab268b1e08b7860fb6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 07:15:03 +0100 Subject: [PATCH 34/53] Adapt test for new limit/offset rule for hg --- .../scm/repository/spi/HgBrowseCommandTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index c017d0632c..e099720d1f 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -183,21 +183,21 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { @Test public void testLimit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); FileObject root = result.getFile(); Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("c", "a.txt"); + assertThat(foList).extracting("name").containsExactly("c", "a.txt"); assertThat(root.isTruncated()).isTrue(); } @Test public void testOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); request.setOffset(2); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); @@ -205,7 +205,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); - assertThat(foList).extracting("name").containsExactlyInAnyOrder("b.txt", "f.txt"); + assertThat(foList).extracting("name").containsExactly("b.txt", "f.txt"); assertThat(root.isTruncated()).isFalse(); } @@ -214,7 +214,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testRecursiveLimit() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(4); + request.setLimit(3); request.setRecursive(true); FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); @@ -237,7 +237,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testRecursiveLimitInSubDir() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(2); + request.setLimit(1); request.setRecursive(true); FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); @@ -260,7 +260,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { public void testRecursiveOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setOffset(2); + request.setOffset(1); request.setRecursive(true); FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile(); From f2ce14294dc7b8165523af11d6ddfc595b8e2098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 09:30:25 +0100 Subject: [PATCH 35/53] Extract parsing of fileview stream and bootstrap tests --- .../spi/javahg/HgFileviewCommand.java | 140 +------------- .../javahg/HgFileviewCommandResultReader.java | 158 ++++++++++++++++ .../HgFileviewCommandResultReaderTest.java | 171 ++++++++++++++++++ 3 files changed, 330 insertions(+), 139 deletions(-) create mode 100644 scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java create mode 100644 scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java index 815e830c8a..99c0623d97 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java @@ -35,23 +35,16 @@ package sonia.scm.repository.spi.javahg; //~--- non-JDK imports -------------------------------------------------------- -import com.aragost.javahg.DateTime; import com.aragost.javahg.Repository; import com.aragost.javahg.internals.AbstractCommand; import com.aragost.javahg.internals.HgInputStream; -import com.google.common.base.Strings; - import sonia.scm.repository.FileObject; -import sonia.scm.repository.SubRepository; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; -import java.util.Deque; -import java.util.LinkedList; - /** * Mercurial command to list files of a repository. * @@ -60,7 +53,6 @@ import java.util.LinkedList; public class HgFileviewCommand extends AbstractCommand { - public static final char TRUNCATED_MARK = 't'; private boolean disableLastCommit = false; private HgFileviewCommand(Repository repository) @@ -182,139 +174,9 @@ public class HgFileviewCommand extends AbstractCommand { cmdAppend("-t"); - Deque stack = new LinkedList<>(); - HgInputStream stream = launchStream(); - FileObject last = null; - while (stream.peek() != -1 && stream.peek() != TRUNCATED_MARK) { - FileObject file = read(stream); - - while (!stack.isEmpty()) { - FileObject current = stack.peek(); - if (isParent(current, file)) { - current.addChild(file); - break; - } else { - stack.pop(); - } - } - - if (file.isDirectory()) { - stack.push(file); - } - last = file; - } - - if (stack.isEmpty()) { - // if the stack is empty, the requested path is probably a file - return last; - } else { - // if the stack is not empty, the requested path is a directory - if (stream.read() == TRUNCATED_MARK) { - stack.getLast().setTruncated(true); - } - return stack.getLast(); - } - } - - private FileObject read(HgInputStream stream) throws IOException { - char type = (char) stream.read(); - - FileObject file; - switch (type) { - case 'd': - file = readDirectory(stream); - break; - case 'f': - file = readFile(stream); - break; - case 's': - file = readSubRepository(stream); - break; - default: - throw new IOException("unknown file object type: " + type); - } - return file; - } - - private boolean isParent(FileObject parent, FileObject child) { - String parentPath = parent.getPath(); - if (parentPath.equals("")) { - return true; - } - return child.getParentPath().equals(parentPath); - } - - private FileObject readDirectory(HgInputStream stream) throws IOException { - FileObject directory = new FileObject(); - String path = removeTrailingSlash(stream.textUpTo('\0')); - - directory.setName(getNameFromPath(path)); - directory.setDirectory(true); - directory.setPath(path); - - return directory; - } - - private FileObject readFile(HgInputStream stream) throws IOException { - FileObject file = new FileObject(); - String path = removeTrailingSlash(stream.textUpTo('\n')); - - file.setName(getNameFromPath(path)); - file.setPath(path); - file.setDirectory(false); - file.setLength((long) stream.decimalIntUpTo(' ')); - - DateTime timestamp = stream.dateTimeUpTo(' '); - String description = stream.textUpTo('\0'); - - if (!disableLastCommit) { - file.setCommitDate(timestamp.getDate().getTime()); - file.setDescription(description); - } - - return file; - } - - private FileObject readSubRepository(HgInputStream stream) throws IOException { - FileObject directory = new FileObject(); - String path = removeTrailingSlash(stream.textUpTo('\n')); - - directory.setName(getNameFromPath(path)); - directory.setDirectory(true); - directory.setPath(path); - - String revision = stream.textUpTo(' '); - String url = stream.textUpTo('\0'); - - SubRepository subRepository = new SubRepository(url); - - if (!Strings.isNullOrEmpty(revision)) { - subRepository.setRevision(revision); - } - - directory.setSubRepository(subRepository); - - return directory; - } - - private String removeTrailingSlash(String path) { - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - - return path; - } - - private String getNameFromPath(String path) { - int index = path.lastIndexOf('/'); - - if (index > 0) { - path = path.substring(index + 1); - } - - return path; + return new HgFileviewCommandResultReader(stream, disableLastCommit).parseResult(); } /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java new file mode 100644 index 0000000000..b93b9b2fb0 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java @@ -0,0 +1,158 @@ +package sonia.scm.repository.spi.javahg; + +import com.aragost.javahg.DateTime; +import com.aragost.javahg.internals.HgInputStream; +import com.google.common.base.Strings; +import sonia.scm.repository.FileObject; +import sonia.scm.repository.SubRepository; + +import java.io.IOException; +import java.util.Deque; +import java.util.LinkedList; + +class HgFileviewCommandResultReader { + + private static final char TRUNCATED_MARK = 't'; + + private final HgInputStream stream; + private final boolean disableLastCommit; + + HgFileviewCommandResultReader(HgInputStream stream, boolean disableLastCommit) { + this.stream = stream; + this.disableLastCommit = disableLastCommit; + } + + FileObject parseResult() throws IOException { + Deque stack = new LinkedList<>(); + + FileObject last = null; + while (stream.peek() != -1 && stream.peek() != TRUNCATED_MARK) { + FileObject file = read(stream); + + while (!stack.isEmpty()) { + FileObject current = stack.peek(); + if (isParent(current, file)) { + current.addChild(file); + break; + } else { + stack.pop(); + } + } + + if (file.isDirectory()) { + stack.push(file); + } + last = file; + } + + if (stack.isEmpty()) { + // if the stack is empty, the requested path is probably a file + return last; + } else { + // if the stack is not empty, the requested path is a directory + if (stream.read() == TRUNCATED_MARK) { + stack.getLast().setTruncated(true); + } + return stack.getLast(); + } + } + + private FileObject read(HgInputStream stream) throws IOException { + char type = (char) stream.read(); + + FileObject file; + switch (type) { + case 'd': + file = readDirectory(stream); + break; + case 'f': + file = readFile(stream); + break; + case 's': + file = readSubRepository(stream); + break; + default: + throw new IOException("unknown file object type: " + type); + } + return file; + } + + private boolean isParent(FileObject parent, FileObject child) { + String parentPath = parent.getPath(); + if (parentPath.equals("")) { + return true; + } + return child.getParentPath().equals(parentPath); + } + + private FileObject readDirectory(HgInputStream stream) throws IOException { + FileObject directory = new FileObject(); + String path = removeTrailingSlash(stream.textUpTo('\0')); + + directory.setName(getNameFromPath(path)); + directory.setDirectory(true); + directory.setPath(path); + + return directory; + } + + private FileObject readFile(HgInputStream stream) throws IOException { + FileObject file = new FileObject(); + String path = removeTrailingSlash(stream.textUpTo('\n')); + + file.setName(getNameFromPath(path)); + file.setPath(path); + file.setDirectory(false); + file.setLength((long) stream.decimalIntUpTo(' ')); + + DateTime timestamp = stream.dateTimeUpTo(' '); + String description = stream.textUpTo('\0'); + + if (!disableLastCommit) { + file.setCommitDate(timestamp.getDate().getTime()); + file.setDescription(description); + } + + return file; + } + + private FileObject readSubRepository(HgInputStream stream) throws IOException { + FileObject directory = new FileObject(); + String path = removeTrailingSlash(stream.textUpTo('\n')); + + directory.setName(getNameFromPath(path)); + directory.setDirectory(true); + directory.setPath(path); + + String revision = stream.textUpTo(' '); + String url = stream.textUpTo('\0'); + + SubRepository subRepository = new SubRepository(url); + + if (!Strings.isNullOrEmpty(revision)) { + subRepository.setRevision(revision); + } + + directory.setSubRepository(subRepository); + + return directory; + } + + private String removeTrailingSlash(String path) { + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + return path; + } + + private String getNameFromPath(String path) { + int index = path.lastIndexOf('/'); + + if (index > 0) { + path = path.substring(index + 1); + } + + return path; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java new file mode 100644 index 0000000000..33ed321867 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java @@ -0,0 +1,171 @@ +package sonia.scm.repository.spi.javahg; + +import com.aragost.javahg.internals.HgInputStream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import sonia.scm.repository.FileObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.OptionalLong; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; + +class HgFileviewCommandResultReaderTest { + + @Test + void shouldParseSimpleAttributes() throws IOException { + Instant time1 = Instant.now().truncatedTo(ChronoUnit.SECONDS); + Instant time2 = time1.minus(1, ChronoUnit.DAYS); + + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("dir") + .file("a.txt", 10, time1.toEpochMilli(), "file a") + .file("b.txt", 100, time2.toEpochMilli(), "file b\nwith some\nmore text") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.isDirectory()).isTrue(); + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "a.txt", "b.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false, false); + assertThat(fileObject.getChildren()) + .extracting("length") + .containsExactly(OptionalLong.empty(), OptionalLong.of(10L), OptionalLong.of(100L)); + assertThat(fileObject.getChildren()) + .extracting("description") + .containsExactly(empty(), of("file a"), of("file b\nwith some\nmore text")); + assertThat(fileObject.getChildren()) + .extracting("commitDate") + .containsExactly(OptionalLong.empty(), OptionalLong.of(time1.toEpochMilli()), OptionalLong.of(time2.toEpochMilli())); + assertThat(fileObject.isTruncated()).isFalse(); + } + + @Test + void shouldParseTruncatedFlag() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("dir") + .file("a.txt") + .truncated(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.isTruncated()).isTrue(); + } + + @Test + void shouldParseRecursiveResult() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("dir") + .dir("dir/more") + .file("dir/more/c.txt") + .file("dir/a.txt") + .file("dir/b.txt") + .file("d.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "d.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subDir = fileObject.getChildren().iterator().next(); + assertThat(subDir.getChildren()) + .extracting("name") + .containsExactly("more", "a.txt", "b.txt"); + assertThat(subDir.getChildren()) + .extracting("directory") + .containsExactly(true, false, false); + + FileObject subSubDir = subDir.getChildren().iterator().next(); + assertThat(subSubDir.getChildren()) + .extracting("name") + .containsExactly("c.txt"); + assertThat(subSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + } + + @Test + void shouldIgnoreTimeAndCommentWhenDisabled() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .dir("c") + .file("a.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("description") + .containsOnly(empty()); + assertThat(fileObject.getChildren()) + .extracting("commitDate") + .containsOnly(OptionalLong.empty()); + } + + private HgInputStream createInputStream(String input) { + return new HgInputStream(new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.newDecoder()); + } + + private class MockInput { + private final StringBuilder stringBuilder = new StringBuilder(); + private boolean disableLastCommit = false; + + MockInput dir(String name) { + stringBuilder + .append('d') + .append(name) + .append('/') + .append('\0'); + return this; + } + + MockInput file(String name) { + disableLastCommit = true; + return file(name, 1024, 0, "n/a"); + } + + MockInput file(String name, int length, long time, String comment) { + stringBuilder + .append('f') + .append(name) + .append('\n') + .append(length) + .append(' ') + .append(time/1000) + .append(' ') + .append(0) + .append(' ') + .append(comment) + .append('\0'); + return this; + } + + HgFileviewCommandResultReader truncated() { + stringBuilder.append("t"); + return build(); + } + + HgFileviewCommandResultReader build() { + HgInputStream inputStream = createInputStream(stringBuilder.toString()); + return new HgFileviewCommandResultReader(inputStream, disableLastCommit); + } + } +} From cb1d9334bb2d06e5a84eaff82903c802094f2d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 10:54:35 +0100 Subject: [PATCH 36/53] Remove unnecessary check --- .../javahg/HgFileviewCommandResultReader.java | 3 --- .../HgFileviewCommandResultReaderTest.java | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java index b93b9b2fb0..c8d03c7d8e 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java @@ -79,9 +79,6 @@ class HgFileviewCommandResultReader { private boolean isParent(FileObject parent, FileObject child) { String parentPath = parent.getPath(); - if (parentPath.equals("")) { - return true; - } return child.getParentPath().equals(parentPath); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java index 33ed321867..fa299877a8 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java @@ -64,6 +64,22 @@ class HgFileviewCommandResultReaderTest { assertThat(fileObject.isTruncated()).isTrue(); } + @Test + void shouldParseSubDirectory() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("dir") + .file("dir/a.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.isDirectory()).isTrue(); + assertThat(fileObject.getName()).isEqualTo("dir"); + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("a.txt"); + } + @Test void shouldParseRecursiveResult() throws IOException { HgFileviewCommandResultReader reader = new MockInput() From 7aa91bf8461eb9b006b23a28b1cf93a8b94bfd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 11:47:30 +0100 Subject: [PATCH 37/53] Implement simple implicit directories --- .../javahg/HgFileviewCommandResultReader.java | 35 +++++++++++ .../HgFileviewCommandResultReaderTest.java | 62 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java index c8d03c7d8e..d62ab07f1c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java @@ -7,6 +7,8 @@ import sonia.scm.repository.FileObject; import sonia.scm.repository.SubRepository; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.Deque; import java.util.LinkedList; @@ -34,6 +36,15 @@ class HgFileviewCommandResultReader { if (isParent(current, file)) { current.addChild(file); break; + } else if (isAncestor(current, file)) { + Collection missingParents = createMissingParents(current, file); + for (FileObject subDir : missingParents) { + current.addChild(subDir); + stack.push(subDir); + current = stack.peek(); + } + current.addChild(file); + break; } else { stack.pop(); } @@ -82,6 +93,30 @@ class HgFileviewCommandResultReader { return child.getParentPath().equals(parentPath); } + private boolean isAncestor(FileObject ancestor, FileObject child) { + String ancestorPath = ancestor.getPath(); + return child.getParentPath().startsWith(ancestorPath); + } + + private Collection createMissingParents(FileObject current, FileObject file) { + String missingPath = file.getPath().substring(current.getPath().length(), file.getPath().lastIndexOf('/')); + + FileObject directory = new FileObject(); + directory.setName(getNameFromPath(missingPath)); + directory.setDirectory(true); + directory.setPath(missingPath); + + Collection parents = new ArrayList<>(); + + if (!isParent(current, directory)) { + parents.addAll(createMissingParents(current, directory)); + } + + parents.add(directory); + + return parents; + } + private FileObject readDirectory(HgInputStream stream) throws IOException { FileObject directory = new FileObject(); String path = removeTrailingSlash(stream.textUpTo('\0')); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java index fa299877a8..9426cc4533 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java @@ -118,6 +118,68 @@ class HgFileviewCommandResultReaderTest { .containsExactly(false); } + @Test + void shouldCreateDirectoriesImplicitly() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .file("dir/a.txt") + .file("dir/b.txt") + .file("d.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "d.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subDir = fileObject.getChildren().iterator().next(); + assertThat(subDir.getChildren()) + .extracting("name") + .containsExactly("a.txt", "b.txt"); + assertThat(subDir.getChildren()) + .extracting("directory") + .containsExactly(false, false); + } + + @Test + void shouldCreateSubSubDirectoriesImplicitly() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .file("dir/more/a.txt") + .file("dir/b.txt") + .file("d.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "d.txt"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subDir = fileObject.getChildren().iterator().next(); + assertThat(subDir.getChildren()) + .extracting("name") + .containsExactly("more", "b.txt"); + assertThat(subDir.getChildren()) + .extracting("directory") + .containsExactly(true, false); + + FileObject subSubDir = subDir.getChildren().iterator().next(); + assertThat(subSubDir.getChildren()) + .extracting("name") + .containsExactly("a.txt"); + assertThat(subSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + } + @Test void shouldIgnoreTimeAndCommentWhenDisabled() throws IOException { HgFileviewCommandResultReader reader = new MockInput() From 8b4dd2642511ad26ba672d2df0c420fc91d6b1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 12:03:58 +0100 Subject: [PATCH 38/53] Implement more complex implicit directories --- .../javahg/HgFileviewCommandResultReader.java | 2 +- .../HgFileviewCommandResultReaderTest.java | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java index d62ab07f1c..c3c2efe4b5 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReader.java @@ -95,7 +95,7 @@ class HgFileviewCommandResultReader { private boolean isAncestor(FileObject ancestor, FileObject child) { String ancestorPath = ancestor.getPath(); - return child.getParentPath().startsWith(ancestorPath); + return ancestorPath.equals("") || child.getParentPath().startsWith(ancestorPath + '/'); } private Collection createMissingParents(FileObject current, FileObject file) { diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java index 9426cc4533..806896b1f8 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/javahg/HgFileviewCommandResultReaderTest.java @@ -9,6 +9,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Iterator; import java.util.OptionalLong; import static java.nio.charset.StandardCharsets.UTF_8; @@ -180,6 +181,41 @@ class HgFileviewCommandResultReaderTest { .containsExactly(false); } + @Test + void shouldCreateSimilarSubDirectoriesCorrectly() throws IOException { + HgFileviewCommandResultReader reader = new MockInput() + .dir("") + .file("dir/a.txt") + .file("directory/b.txt") + .build(); + + FileObject fileObject = reader.parseResult(); + + assertThat(fileObject.getChildren()) + .extracting("name") + .containsExactly("dir", "directory"); + assertThat(fileObject.getChildren()) + .extracting("directory") + .containsExactly(true, true); + + Iterator fileIterator = fileObject.getChildren().iterator(); + FileObject firstSubDir = fileIterator.next(); + assertThat(firstSubDir.getChildren()) + .extracting("name") + .containsExactly("a.txt"); + assertThat(firstSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + + FileObject secondSubDir = fileIterator.next(); + assertThat(secondSubDir.getChildren()) + .extracting("name") + .containsExactly("b.txt"); + assertThat(secondSubDir.getChildren()) + .extracting("directory") + .containsExactly(false); + } + @Test void shouldIgnoreTimeAndCommentWhenDisabled() throws IOException { HgFileviewCommandResultReader reader = new MockInput() From c7b466f3c6e2f9198a1126aefad45986a8e70817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 14:01:53 +0100 Subject: [PATCH 39/53] Fix offset calculation for hg --- .../main/resources/sonia/scm/hg/ext/fileview.py | 12 ++++++------ .../resources/sonia/scm/hg/ext/fileview_test.py | 17 ++++++++--------- .../scm/repository/spi/HgBrowseCommandTest.java | 7 +++---- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index b303b2f3d4..fdc119f274 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -211,18 +211,21 @@ class File_Printer: self.revCtx = revCtx self.disableLastCommit = disableLastCommit self.transport = transport - self.result_count = -1 + self.result_count = 0 + self.initial_path_printed = False self.limit = limit self.offset = offset def print_directory(self, path): - if self.shouldPrintResult(): + if not self.initial_path_printed or self.offset == 0 or self.shouldPrintResult(): + self.initial_path_printed = True format = '%s/\n' if self.transport: format = 'd%s/\0' self.writer.write( format % path) def print_file(self, path): + self.result_count += 1 if self.shouldPrintResult(): file = self.revCtx[path] date = '0 0' @@ -252,10 +255,7 @@ class File_Printer: self.print_file(file.path) def shouldPrintResult(self): - # The first result is the selected path (or root if not specified). This - # always has to be printed. Therefore we start counting with -1. - self.result_count += 1 - return self.result_count == 0 or self.offset < self.result_count <= self.limit + self.offset + return self.offset < self.result_count <= self.limit + self.offset def isTruncated(self): return self.result_count > self.limit + self.offset diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py index 13be49c5a9..2a4436470a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py @@ -81,18 +81,17 @@ class Test_File_Viewer(unittest.TestCase): def test_printer(self): paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] writer = self.view_with_limit_and_offset(paths, 1000, 0) - self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) + self.assertPaths(writer, ["/", "c/g/h.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) def test_printer_with_limit(self): paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] - writer = self.view_with_limit_and_offset(paths, 3, 0) - self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt"]) + writer = self.view_with_limit_and_offset(paths, 1, 0) + self.assertPaths(writer, ["/", "c/g/h.txt"]) - # TODO fix - def x_test_printer_with_offset(self): - paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] - writer = self.view_with_limit_and_offset(paths, 100, 3) - self.assertPaths(writer, ["/", "c", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) + def test_printer_with_offset(self): + paths = ["c/g/h.txt", "c/g/i.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"] + writer = self.view_with_limit_and_offset(paths, 100, 1) + self.assertPaths(writer, ["/", "c/g/i.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) def view_with_limit_and_offset(self, paths, limit, offset): revCtx = DummyRevContext(paths) @@ -113,9 +112,9 @@ class Test_File_Viewer(unittest.TestCase): self.assertTrue(nextChar == " " or nextChar == "\n", expected + " does not match " + actual) def assertPaths(self, actual, expected): + self.assertEqual(len(actual), len(expected)) for idx,item in enumerate(actual): self.assertPath(item, expected[idx]) - self.assertEqual(len(actual), len(expected)) def test_recursive_with_path(self): root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index e099720d1f..52bc1d0535 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -197,8 +197,8 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { @Test public void testOffset() throws IOException { BrowseCommandRequest request = new BrowseCommandRequest(); - request.setLimit(1); - request.setOffset(2); + request.setLimit(2); + request.setOffset(1); BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request); FileObject root = result.getFile(); @@ -308,8 +308,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { Collection foList = root.getChildren(); assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(4, foList.size()); + assertThat(foList).extracting("name").containsExactly("c", "a.txt", "b.txt", "f.txt"); return foList; } From a6b45ce55615292066e27d8538180ed53047f4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Mar 2020 14:39:36 +0100 Subject: [PATCH 40/53] Fig python tests --- .../main/resources/sonia/scm/hg/ext/fileview_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py index 2a4436470a..bce3fa0ec0 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py @@ -26,7 +26,7 @@ class File_Object_Collector(): def __init__(self): self.stack = [] - + def __getitem__(self, key): if len(self.stack) == 0 and key == 0: return self.last @@ -43,7 +43,7 @@ class File_Object_Collector(): if file.directory: self.stack.append(file) self.last = file - + class CollectingWriter: def __init__(self): self.stack = [] @@ -81,12 +81,12 @@ class Test_File_Viewer(unittest.TestCase): def test_printer(self): paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] writer = self.view_with_limit_and_offset(paths, 1000, 0) - self.assertPaths(writer, ["/", "c/g/h.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) + self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"]) def test_printer_with_limit(self): paths = ["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"] writer = self.view_with_limit_and_offset(paths, 1, 0) - self.assertPaths(writer, ["/", "c/g/h.txt"]) + self.assertPaths(writer, ["/", "c/", "c/g/", "c/g/h.txt"]) def test_printer_with_offset(self): paths = ["c/g/h.txt", "c/g/i.txt", "c/d.txt", "c/e.txt", "a", "b", "f.txt"] @@ -177,7 +177,7 @@ class Test_File_Viewer(unittest.TestCase): self.assertEqual(len(parent), len(expectedPaths)) for idx,item in enumerate(parent.children): self.assertEqual(item.path, expectedPaths[idx]) - + def assertFile(self, file, expectedPath): self.assertEquals(file.path, expectedPath) self.assertFalse(file.directory) From 4ddfbcd86e71278ff87240a98db243651a872ce1 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 6 Mar 2020 16:17:47 +0100 Subject: [PATCH 41/53] Sort directories correctly --- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index b303b2f3d4..7c43ae65ae 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -153,7 +153,7 @@ class File_Walker: return path def walk(self, structure, parent = ""): - sortedItems = sorted(structure.iteritems(), key = lambda item: item[1]) + sortedItems = sorted(structure.iteritems(), key = lambda item: self.sortKey(item)) for key, value in sortedItems: if key == FILE_MARKER: if value: @@ -166,6 +166,12 @@ class File_Walker: else: self.visit_directory(self.create_path(parent, value)) + def sortKey(self, item): + if (item[0] == FILE_MARKER): + return "2" + else: + return "1" + item[0] + class SubRepository: url = None revision = None From 845a3ea72f8a71f97b2870ebd3c2ad738b42d047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 9 Mar 2020 09:20:08 +0100 Subject: [PATCH 42/53] Check order of files in test --- .../sonia/scm/repository/spi/HgBrowseCommandTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java index 52bc1d0535..aa786bb693 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java @@ -66,6 +66,11 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { @Test public void testBrowse() throws IOException { Collection foList = getRootFromTip(new BrowseCommandRequest()); + + assertThat(foList) + .extracting("name") + .containsExactly("c", "a.txt", "b.txt", "f.txt"); + FileObject a = getFileObject(foList, "a.txt"); FileObject c = getFileObject(foList, "c"); @@ -108,6 +113,10 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase { assertEquals("c", c.getName()); Collection foList = c.getChildren(); + assertThat(foList) + .extracting("name") + .containsExactly("d.txt", "e.txt"); + assertNotNull(foList); assertFalse(foList.isEmpty()); assertEquals(2, foList.size()); From df1cc365edec4a622a40041d36ab841aecdf9e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 9 Mar 2020 09:23:53 +0100 Subject: [PATCH 43/53] Cleanup test --- .../scm/repository/spi/SvnBrowseCommandTest.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java index 6ddebf0bf9..fb12e6ff3d 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java @@ -70,13 +70,9 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase assertNotNull(result); - Collection foList1 = result.getFile().getChildren(); + Collection foList = result.getFile().getChildren(); - assertNotNull(foList1); - assertFalse(foList1.isEmpty()); - assertEquals(2, foList1.size()); - - Collection foList = foList1; + assertThat(foList).extracting("name").containsExactly("c", "a.txt"); Iterator iterator = foList.iterator(); FileObject c = iterator.next(); @@ -111,9 +107,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase Collection foList = result.getFile().getChildren(); - assertNotNull(foList); - assertFalse(foList.isEmpty()); - assertEquals(2, foList.size()); + assertThat(foList).extracting("name").containsExactly("d.txt", "e.txt"); Iterator iterator = foList.iterator(); FileObject d = iterator.next(); From fa889529a29face3c2300b71b8db99bd462ddea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 9 Mar 2020 09:48:14 +0100 Subject: [PATCH 44/53] Only count files in frontend --- scm-ui/ui-webapp/public/locales/de/repos.json | 2 +- scm-ui/ui-webapp/public/locales/en/repos.json | 2 +- scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx | 6 +++--- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 2b7b0f2ca4..175abf863b 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -130,7 +130,7 @@ "notBound": "Keine Erweiterung angebunden." }, "loadMore": "Laden", - "moreEntriesAvailable": "Es werden nur die ersten {{count}} Einträge angezeigt. Es sind weitere Einträge vorhanden." + "moreFilesAvailable": "Es werden nur die ersten {{count}} Dateien angezeigt. Es sind weitere Dateien vorhanden." }, "permission": { "title": "Berechtigungen bearbeiten", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index d5a5a7fff6..21fe49f95d 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -130,7 +130,7 @@ "notBound": "No extension bound." }, "loadMore": "Load", - "moreEntriesAvailable": "These are just the first {{count}} entries. There are more entries available." + "moreFilesAvailable": "These are just the first {{count}} files. There are more files available." }, "permission": { "title": "Edit Permissions", diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index df70b44500..b10f0878d7 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -92,15 +92,15 @@ class FileTree extends React.Component { renderTruncatedInfo = () => { const { hunks, t } = this.props; const lastHunk = hunks[hunks.length - 1]; - const entryCount = hunks + const fileCount = hunks .filter(hunk => hunk?.tree?._embedded?.children) - .map(hunk => hunk.tree._embedded.children.length) + .map(hunk => hunk.tree._embedded.children.filter(c => !c.directory).length) .reduce((a, b) => a + b, 0); if (lastHunk.tree?.truncated) { return (
-
{t("sources.moreEntriesAvailable", { count: entryCount })}
+
{t("sources.moreFilesAvailable", { count: fileCount })}
diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 857229f8c2..779a554aa4 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -31,7 +31,7 @@ export function fetchSources(repository: Repository, revision: string, path: str for (let i = 0; i < hunk; ++i) { const sources = getSources(state, repository, revision, path, i); if (sources?._embedded.children) { - offset += sources._embedded.children.length; + offset += sources._embedded.children.filter(c => !c.directory).length; } } From 85a893824cdfea6376a325205645333d93b5d35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 9 Mar 2020 16:10:08 +0100 Subject: [PATCH 45/53] Log change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa1f5ff79..34e0e02fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added footer extension points for links and avatar - Create OpenAPI specification during build - Extension point entries with supplied extensionName are sorted ascending +- By default, only 100 files will be listed in source view in one request ### Changed - New footer design From 0927e3e4211163310c7a0cfe80d6bfec7196bee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 9 Mar 2020 16:53:17 +0100 Subject: [PATCH 46/53] Fix error handling --- .../ui-webapp/src/repos/sources/components/FileTree.tsx | 9 ++++++--- scm-ui/ui-webapp/src/repos/sources/modules/sources.ts | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index b10f0878d7..6a9186b3e7 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -115,8 +115,8 @@ class FileTree extends React.Component { return null; } - if (hunks.some(hunk => hunk.error)) { - return hunk.error)[0]} />; + if (hunks[0]?.error) { + return ; } return ( @@ -183,6 +183,7 @@ class FileTree extends React.Component { {hunks[hunks.length - 1].loading && } + {hunks[hunks.length - 1].error && } ); } @@ -210,9 +211,11 @@ const mapStateToProps = (state: any, ownProps: Props) => { for (let i = 0; i < hunkCount; ++i) { const tree = getSources(state, repository, revision, path, i); const loading = isFetchSourcesPending(state, repository, revision, path, i); + const error = getFetchSourcesFailure(state, repository, revision, path, i); hunks.push({ tree, - loading + loading, + error }); } diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index 779a554aa4..cb9771a181 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -103,7 +103,7 @@ export function fetchSourcesFailure( ): Action { return { type: FETCH_SOURCES_FAILURE, - payload: error, + payload: { hunk, pending: false, updatePending: false, error }, itemId: createItemId(repository, revision, path, "") }; } @@ -122,12 +122,13 @@ export default function reducer( type: "UNKNOWN" } ): any { - if (action.itemId && action.type === FETCH_SOURCES_SUCCESS) { + if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_SOURCES_FAILURE)) { return { ...state, [action.itemId + "hunkCount"]: action.payload.hunk + 1, [action.itemId + action.payload.hunk]: { sources: action.payload.sources, + error: action.payload.error, updatePending: false, pending: false } @@ -220,5 +221,5 @@ export function getFetchSourcesFailure( path: string, hunk = 0 ): Error | null | undefined { - return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path, "")); + return state.sources[createItemId(repository, revision, path, hunk)]?.error; } From 5a3bb52b72ac1c99bbc7a8b02f221d4b892b6efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 10 Mar 2020 09:28:03 +0100 Subject: [PATCH 47/53] Fix update of sources An Update of sources must not set the hunk count. --- .../src/repos/sources/modules/sources.test.ts | 10 ++++-- .../src/repos/sources/modules/sources.ts | 36 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts index f49488a7df..1c819c91f1 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.test.ts @@ -295,14 +295,18 @@ describe("selector tests", () => { it("should return error when fetch sources did fail", () => { const state = { - failure: { - [FETCH_SOURCES + "/scm/core/_//"]: error + sources: { + "scm/core/_//0": { + pending: false, + sources: {}, + error: error + } } }; expect(getFetchSourcesFailure(state, repository, "", "", 0)).toEqual(error); }); it("should return undefined when fetch sources did not fail", () => { - expect(getFetchSourcesFailure({}, repository, "", "", 0)).toBe(undefined); + expect(getFetchSourcesFailure({}, repository, "", "", 0)).toBe(null); }); }); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index cb9771a181..4c78c3525f 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -7,6 +7,7 @@ export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES"; export const FETCH_SOURCES_PENDING = `${FETCH_SOURCES}_${types.PENDING_SUFFIX}`; export const FETCH_UPDATES_PENDING = `${FETCH_SOURCES}_UPDATE_PENDING`; export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`; +export const FETCH_UPDATES_SUCCESS = `${FETCH_SOURCES}_UPDATE_SUCCESS`; export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`; export function fetchSources(repository: Repository, revision: string, path: string, initialLoad = true, hunk = 0) { @@ -39,7 +40,11 @@ export function fetchSources(repository: Repository, revision: string, path: str .get(createUrl(repository, revision, path, offset)) .then(response => response.json()) .then((sources: File) => { - dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); + if (initialLoad) { + dispatch(fetchSourcesSuccess(repository, revision, path, hunk, sources)); + } else { + dispatch(fetchUpdatesSuccess(repository, revision, path, hunk, sources)); + } }) .catch(err => { dispatch(fetchSourcesFailure(repository, revision, path, hunk, err)); @@ -94,6 +99,20 @@ export function fetchSourcesSuccess( }; } +export function fetchUpdatesSuccess( + repository: Repository, + revision: string, + path: string, + hunk: number, + sources: File +) { + return { + type: FETCH_UPDATES_SUCCESS, + payload: { hunk, pending: false, updatePending: false, sources }, + itemId: createItemId(repository, revision, path, "") + }; +} + export function fetchSourcesFailure( repository: Repository, revision: string, @@ -133,6 +152,16 @@ export default function reducer( pending: false } }; + } else if (action.itemId && (action.type === FETCH_UPDATES_SUCCESS)) { + return { + ...state, + [action.itemId + action.payload.hunk]: { + sources: action.payload.sources, + error: action.payload.error, + updatePending: false, + pending: false + } + }; } else if (action.itemId && action.type === FETCH_UPDATES_PENDING) { return { ...state, @@ -221,5 +250,8 @@ export function getFetchSourcesFailure( path: string, hunk = 0 ): Error | null | undefined { - return state.sources[createItemId(repository, revision, path, hunk)]?.error; + if (state.sources) { + return state.sources && state.sources[createItemId(repository, revision, path, hunk)]?.error; + } + return null; } From daff8c56943666f82e6f7b8d95bb892e29cb2a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Mar 2020 10:40:32 +0100 Subject: [PATCH 48/53] Fix typo --- .../sonia/scm/repository/spi/BrowseCommandTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java index 1ed39c8366..8b429b4a7f 100644 --- a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java @@ -24,7 +24,7 @@ class BrowseCommandTest implements BrowseCommand { f("README") ); - sort(entries, Entry::isDirecotry, Entry::getName); + sort(entries, Entry::isDirectory, Entry::getName); assertThat(entries).extracting("name") .containsExactly( @@ -44,7 +44,7 @@ class BrowseCommandTest implements BrowseCommand { static class Entry { private final String name; - private final boolean direcotry; + private final boolean directory; static Entry f(String name) { return new Entry(name, false); @@ -54,17 +54,17 @@ class BrowseCommandTest implements BrowseCommand { return new Entry(name, true); } - public Entry(String name, boolean direcotry) { + public Entry(String name, boolean directory) { this.name = name; - this.direcotry = direcotry; + this.directory = directory; } public String getName() { return name; } - public boolean isDirecotry() { - return direcotry; + public boolean isDirectory() { + return directory; } } } From 36c173ccbd32ef307e757f8496db6bac4a1b098b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Mar 2020 10:44:00 +0100 Subject: [PATCH 49/53] Use concrete names --- .../scm/repository/spi/BrowseCommandTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java index 8b429b4a7f..1f1c9ffe20 100644 --- a/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BrowseCommandTest.java @@ -8,20 +8,20 @@ import java.util.List; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; -import static sonia.scm.repository.spi.BrowseCommandTest.Entry.d; -import static sonia.scm.repository.spi.BrowseCommandTest.Entry.f; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.directory; +import static sonia.scm.repository.spi.BrowseCommandTest.Entry.file; class BrowseCommandTest implements BrowseCommand { @Test void shouldSort() { List entries = asList( - f("b.txt"), - f("a.txt"), - f("Dockerfile"), - f(".gitignore"), - d("src"), - f("README") + file("b.txt"), + file("a.txt"), + file("Dockerfile"), + file(".gitignore"), + directory("src"), + file("README") ); sort(entries, Entry::isDirectory, Entry::getName); @@ -46,11 +46,11 @@ class BrowseCommandTest implements BrowseCommand { private final String name; private final boolean directory; - static Entry f(String name) { + static Entry file(String name) { return new Entry(name, false); } - static Entry d(String name) { + static Entry directory(String name) { return new Entry(name, true); } From f39a676044301bfc35c634e970a6c0ef327b670c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Mar 2020 10:45:52 +0100 Subject: [PATCH 50/53] Remove unused function --- .../src/main/java/sonia/scm/repository/GitUtil.java | 5 ----- 1 file changed, 5 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 e816aaf76d..28f64e52cb 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 @@ -744,11 +744,6 @@ 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); - - return getLfsPointer(repo, treeWalk, attributes); - } - - public static Optional getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException { ObjectId blobId = treeWalk.getObjectId(0); return getLfsPointer(repo, blobId, attributes); } From ca34ddf40acdf227347102cb2932dfa6d2caff5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Mar 2020 10:46:47 +0100 Subject: [PATCH 51/53] Harmonize limit default --- .../src/main/resources/sonia/scm/hg/ext/fileview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py index 329587fc8e..94947296bc 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py @@ -307,7 +307,7 @@ class File_Viewer: ('d', 'disableLastCommit', False, 'disables last commit description and date'), ('s', 'disableSubRepositoryDetection', False, 'disables detection of sub repositories'), ('t', 'transport', False, 'format the output for command server'), - ('l', 'limit', 1000, 'limit the number of results'), + ('l', 'limit', 100, 'limit the number of results'), ('o', 'offset', 0, 'proceed from the given result number (zero based)'), ]) def fileview(ui, repo, **opts): From d4977b7d5601748a781a4f68a009d50a7315c169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Mar 2020 10:49:20 +0100 Subject: [PATCH 52/53] Add limit to 'toString' --- .../main/java/sonia/scm/repository/spi/BrowseCommandRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java index c50ab06cee..5e7524c958 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommandRequest.java @@ -149,6 +149,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest .add("recursive", recursive) .add("disableLastCommit", disableLastCommit) .add("disableSubRepositoryDetection", disableSubRepositoryDetection) + .add("limit", limit) .add("offset", offset) .toString(); //J+ From f9122ebb36ed618e6a476b6fc4d7d24ad31256a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 12 Mar 2020 11:18:43 +0100 Subject: [PATCH 53/53] Fix import --- scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx index 6a9186b3e7..2d9d8b1e61 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTree.tsx @@ -15,7 +15,7 @@ import { isFetchSourcesPending } from "../modules/sources"; import FileTreeLeaf from "./FileTreeLeaf"; -import Button from "@scm-manager/ui-components/src/buttons/Button"; +import { Button } from "@scm-manager/ui-components"; type Hunk = { tree: File;