mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-06-20 17:21:49 +02:00
Merge pull request #1032 from scm-manager/feature/browse_commit_with_limit
Feature browse sources with limit and offset
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Extension point entries with supplied extensionName are sorted ascending
|
||||
- Possibility to configure git core config entries for jgit like core.trustfolderstat and core.supportsatomicfilecreation
|
||||
- Babel-plugin-styled-components for persistent generated classnames
|
||||
- By default, only 100 files will be listed in source view in one request
|
||||
|
||||
### Changed
|
||||
- New footer design
|
||||
|
||||
@@ -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<FileObject> children = new ArrayList<>();
|
||||
|
||||
private boolean truncated;
|
||||
}
|
||||
|
||||
@@ -300,6 +300,35 @@ public final class BrowseCommandBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of result files to <code>limit</code> 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 (directories are <b>not</b> counted).
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public BrowseCommandBuilder setLimit(int limit) {
|
||||
request.setLimit(limit);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed the list from the given number on (zero based).
|
||||
*
|
||||
* @param offset The number of the file, the result should start with (zero based).
|
||||
* All preceding files will be omitted. Directories are <b>not</b>
|
||||
* counted. Therefore directories are only listed in results without
|
||||
* offset.
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public BrowseCommandBuilder setOffset(int offset) {
|
||||
request.setOffset(offset);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void updateCache(BrowserResult updatedResult) {
|
||||
if (!disableCache) {
|
||||
CacheKey key = new CacheKey(repository, request);
|
||||
|
||||
@@ -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 <T> void sort(List<T> entries, Function<T, Boolean> isDirectory, Function<T, String> nameOf) {
|
||||
entries.sort((e1, e2) -> {
|
||||
if (isDirectory.apply(e1).equals(isDirectory.apply(e2))) {
|
||||
return nameOf.apply(e1).compareTo(nameOf.apply(e2));
|
||||
} else if (isDirectory.apply(e1)) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,10 @@ import java.util.function.Consumer;
|
||||
public final class BrowseCommandRequest extends FileBaseCommandRequest
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
public static final int DEFAULT_REQUEST_LIMIT = 100;
|
||||
|
||||
private static final long serialVersionUID = 7956624623516803183L;
|
||||
private int offset;
|
||||
|
||||
public BrowseCommandRequest() {
|
||||
this(null);
|
||||
@@ -110,10 +112,12 @@ 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)
|
||||
&& Objects.equal(limit, other.limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +130,7 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(super.hashCode(), recursive, disableLastCommit,
|
||||
disableSubRepositoryDetection);
|
||||
disableSubRepositoryDetection, offset, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,6 +149,8 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
|
||||
.add("recursive", recursive)
|
||||
.add("disableLastCommit", disableLastCommit)
|
||||
.add("disableSubRepositoryDetection", disableSubRepositoryDetection)
|
||||
.add("limit", limit)
|
||||
.add("offset", offset)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
@@ -191,6 +197,28 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
|
||||
this.recursive = recursive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of result files to <code>limit</code> entries.
|
||||
*
|
||||
* @param limit The maximal number of files this request shall return.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public void setLimit(int limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public void setOffset(int offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -232,6 +260,24 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of the entry, the result start with. All preceding entries will be omitted.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public int getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public void updateCache(BrowserResult update) {
|
||||
if (updater != null) {
|
||||
updater.accept(update);
|
||||
@@ -249,6 +295,10 @@ public final class BrowseCommandRequest extends FileBaseCommandRequest
|
||||
/** browse file objects recursive */
|
||||
private boolean recursive = false;
|
||||
|
||||
|
||||
/** Limit the number of result files to <code>limit</code> entries. */
|
||||
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).
|
||||
private final transient Consumer<BrowserResult> updater;
|
||||
|
||||
@@ -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.directory;
|
||||
import static sonia.scm.repository.spi.BrowseCommandTest.Entry.file;
|
||||
|
||||
class BrowseCommandTest implements BrowseCommand {
|
||||
|
||||
@Test
|
||||
void shouldSort() {
|
||||
List<Entry> entries = asList(
|
||||
file("b.txt"),
|
||||
file("a.txt"),
|
||||
file("Dockerfile"),
|
||||
file(".gitignore"),
|
||||
directory("src"),
|
||||
file("README")
|
||||
);
|
||||
|
||||
sort(entries, Entry::isDirectory, 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 directory;
|
||||
|
||||
static Entry file(String name) {
|
||||
return new Entry(name, false);
|
||||
}
|
||||
|
||||
static Entry directory(String name) {
|
||||
return new Entry(name, true);
|
||||
}
|
||||
|
||||
public Entry(String name, boolean directory) {
|
||||
this.name = name;
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -744,14 +744,13 @@ public final class GitUtil
|
||||
|
||||
public static Optional<LfsPointer> getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) throws IOException {
|
||||
Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
|
||||
|
||||
return getLfsPointer(repo, treeWalk, attributes);
|
||||
ObjectId blobId = treeWalk.getObjectId(0);
|
||||
return getLfsPointer(repo, blobId, attributes);
|
||||
}
|
||||
|
||||
public static Optional<LfsPointer> getLfsPointer(org.eclipse.jgit.lib.Repository repo, TreeWalk treeWalk, Attributes attributes) throws IOException {
|
||||
public static Optional<LfsPointer> 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));
|
||||
}
|
||||
|
||||
@@ -69,12 +69,15 @@ 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;
|
||||
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;
|
||||
@@ -110,6 +113,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;
|
||||
@@ -151,18 +156,19 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
fileObject.setName("");
|
||||
fileObject.setPath("");
|
||||
fileObject.setDirectory(true);
|
||||
fileObject.setTruncated(false);
|
||||
return fileObject;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -180,7 +186,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);
|
||||
|
||||
@@ -192,7 +198,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
try (RevWalk walk = new RevWalk(repo)) {
|
||||
commit = walk.parseCommit(revId);
|
||||
}
|
||||
Optional<LfsPointer> lfsPointer = getLfsPointer(repo, path, commit, treeWalk);
|
||||
Optional<LfsPointer> lfsPointer = getLfsPointer(repo, path, commit, treeEntry);
|
||||
|
||||
if (lfsPointer.isPresent()) {
|
||||
setFileLengthFromLfsBlob(lfsPointer.get(), file);
|
||||
@@ -249,31 +255,58 @@ 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<FileObject> files = Lists.newArrayList();
|
||||
while (treeWalk.next())
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
FileObject fileObject = createFileObject(repo, request, revId, treeWalk);
|
||||
if (!fileObject.getPath().startsWith(parent.getPath())) {
|
||||
parent.setChildren(files);
|
||||
return fileObject;
|
||||
private void convertToFileObject(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, List<TreeEntry> entries) throws IOException {
|
||||
List<FileObject> files = Lists.newArrayList();
|
||||
Iterator<TreeEntry> entryIterator = entries.iterator();
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
if (resultCount > request.getOffset() || (request.getOffset() == 0 && fileObject.isDirectory())) {
|
||||
files.add(fileObject);
|
||||
}
|
||||
}
|
||||
|
||||
parent.setChildren(files);
|
||||
|
||||
return null;
|
||||
parent.setTruncated(hasNext);
|
||||
}
|
||||
|
||||
private Optional<TreeEntry> createTree(String path, TreeEntry parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, TreeWalk treeWalk) throws IOException {
|
||||
List<TreeEntry> 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<TreeEntry> 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,
|
||||
@@ -289,7 +322,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();
|
||||
}
|
||||
@@ -329,11 +362,11 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
return null;
|
||||
}
|
||||
|
||||
private Optional<LfsPointer> getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) {
|
||||
private Optional<LfsPointer> 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);
|
||||
}
|
||||
@@ -439,4 +472,54 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
private class TreeEntry {
|
||||
|
||||
private final String pathString;
|
||||
private final String nameString;
|
||||
private final ObjectId objectId;
|
||||
private final boolean directory;
|
||||
private List<TreeEntry> children = emptyList();
|
||||
|
||||
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);
|
||||
ObjectLoader loader = repo.open(objectId);
|
||||
|
||||
this.directory = loader.getType() == Constants.OBJ_TREE;
|
||||
}
|
||||
|
||||
String getPathString() {
|
||||
return pathString;
|
||||
}
|
||||
|
||||
String getNameString() {
|
||||
return nameString;
|
||||
}
|
||||
|
||||
ObjectId getObjectId() {
|
||||
return objectId;
|
||||
}
|
||||
|
||||
boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
List<TreeEntry> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
void setChildren(List<TreeEntry> children) {
|
||||
sort(children, TreeEntry::isDirectory, TreeEntry::getNameString);
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -86,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
|
||||
@@ -99,7 +100,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("a.txt", "c");
|
||||
.containsExactly("c", "a.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -174,7 +175,9 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
Collection<FileObject> 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");
|
||||
@@ -206,7 +209,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");
|
||||
|
||||
@@ -236,6 +239,120 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
||||
.containsExactly(of(42L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowseLimit() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
request.setLimit(1);
|
||||
FileObject root = createCommand()
|
||||
.getBrowserResult(request).getFile();
|
||||
assertNotNull(root);
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList).extracting("name").containsExactly("c", "a.txt");
|
||||
assertThat(foList).hasSize(2);
|
||||
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<FileObject> 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(1);
|
||||
request.setOffset(2);
|
||||
FileObject root = createCommand()
|
||||
.getBrowserResult(request).getFile();
|
||||
assertNotNull(root);
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList).extracting("name").containsExactly("f.txt");
|
||||
assertFalse("result should not be marked as trunctated", root.isTruncated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveLimit() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setLimit(3);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c", "a.txt");
|
||||
|
||||
FileObject c = findFile(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt", "e.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveLimitInSubDir() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setLimit(1);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c");
|
||||
|
||||
FileObject c = findFile(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveOffset() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setOffset(1);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c", "a.txt", "b.txt", "f.txt");
|
||||
|
||||
FileObject c = findFile(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("e.txt");
|
||||
}
|
||||
|
||||
private FileObject findFile(Collection<FileObject> foList, String name) {
|
||||
return foList.stream()
|
||||
.filter(f -> name.equals(f.getName()))
|
||||
|
||||
@@ -101,6 +101,9 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
|
||||
cmd.disableSubRepositoryDetection();
|
||||
}
|
||||
|
||||
cmd.setLimit(request.getLimit());
|
||||
cmd.setOffset(request.getOffset());
|
||||
|
||||
FileObject file = cmd.execute();
|
||||
return new BrowserResult(c == null? "tip": c.getNode(), revision, file);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -141,6 +134,35 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of result files to <code>limit</code> 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 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 setOffset(int offset) {
|
||||
cmdAppend("-o", offset);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the mercurial command and parses the output.
|
||||
*
|
||||
@@ -152,136 +174,9 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
{
|
||||
cmdAppend("-t");
|
||||
|
||||
Deque<FileObject> stack = new LinkedList<>();
|
||||
|
||||
HgInputStream stream = launchStream();
|
||||
|
||||
FileObject last = null;
|
||||
while (stream.peek() != -1) {
|
||||
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
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,5 +189,4 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
{
|
||||
return HgFileviewExtension.NAME;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
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.ArrayList;
|
||||
import java.util.Collection;
|
||||
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<FileObject> 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 if (isAncestor(current, file)) {
|
||||
Collection<FileObject> missingParents = createMissingParents(current, file);
|
||||
for (FileObject subDir : missingParents) {
|
||||
current.addChild(subDir);
|
||||
stack.push(subDir);
|
||||
current = stack.peek();
|
||||
}
|
||||
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();
|
||||
return child.getParentPath().equals(parentPath);
|
||||
}
|
||||
|
||||
private boolean isAncestor(FileObject ancestor, FileObject child) {
|
||||
String ancestorPath = ancestor.getPath();
|
||||
return ancestorPath.equals("") || child.getParentPath().startsWith(ancestorPath + '/');
|
||||
}
|
||||
|
||||
private Collection<FileObject> 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<FileObject> 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'));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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: self.sortKey(item))
|
||||
for key, value in sortedItems:
|
||||
if key == FILE_MARKER:
|
||||
if value:
|
||||
for v in value:
|
||||
@@ -165,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
|
||||
@@ -195,39 +202,55 @@ 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):
|
||||
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
|
||||
self.transport = transport
|
||||
self.result_count = 0
|
||||
self.initial_path_printed = False
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
|
||||
def print_directory(self, path):
|
||||
format = '%s/\n'
|
||||
if self.transport:
|
||||
format = 'd%s/\0'
|
||||
self.ui.write( format % path)
|
||||
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):
|
||||
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) )
|
||||
self.result_count += 1
|
||||
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.writer.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.writer.write( format % (path, subrepo.revision, subrepo.url))
|
||||
|
||||
def visit(self, file):
|
||||
if file.sub_repository:
|
||||
@@ -237,6 +260,19 @@ class File_Printer:
|
||||
else:
|
||||
self.print_file(file.path)
|
||||
|
||||
def shouldPrintResult(self):
|
||||
return self.offset < self.result_count <= self.limit + self.offset
|
||||
|
||||
def isTruncated(self):
|
||||
return self.result_count > self.limit + self.offset
|
||||
|
||||
def finish(self):
|
||||
if self.isTruncated():
|
||||
if self.transport:
|
||||
self.writer.write( "t")
|
||||
else:
|
||||
self.writer.write("truncated")
|
||||
|
||||
class File_Viewer:
|
||||
def __init__(self, revCtx, visitor):
|
||||
self.revCtx = revCtx
|
||||
@@ -271,14 +307,18 @@ 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', 100, 'limit the number of results'),
|
||||
('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"])
|
||||
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
|
||||
viewer.view(opts["path"])
|
||||
printer.finish()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,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
|
||||
@@ -30,7 +43,19 @@ class File_Object_Collector():
|
||||
if file.directory:
|
||||
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):
|
||||
|
||||
@@ -45,19 +70,57 @@ 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_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, 1, 0)
|
||||
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"]
|
||||
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)
|
||||
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):
|
||||
self.assertEqual(len(actual), len(expected))
|
||||
for idx,item in enumerate(actual):
|
||||
self.assertPath(item, expected[idx])
|
||||
|
||||
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 +132,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 +159,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")
|
||||
|
||||
|
||||
@@ -114,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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -67,6 +66,11 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
|
||||
@Test
|
||||
public void testBrowse() throws IOException {
|
||||
Collection<FileObject> 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");
|
||||
|
||||
@@ -109,6 +113,10 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
|
||||
assertEquals("c", c.getName());
|
||||
Collection<FileObject> foList = c.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt", "e.txt");
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(2, foList.size());
|
||||
@@ -181,6 +189,105 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
|
||||
assertEquals(2, c.getChildren().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLimit() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
request.setLimit(1);
|
||||
|
||||
BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request);
|
||||
FileObject root = result.getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
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.setOffset(1);
|
||||
|
||||
BrowserResult result = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request);
|
||||
FileObject root = result.getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList).extracting("name").containsExactly("b.txt", "f.txt");
|
||||
assertThat(root.isTruncated()).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testRecursiveLimit() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setLimit(3);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c", "a.txt");
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt", "e.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveLimitInSubDir() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setLimit(1);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c");
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveOffset() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setOffset(1);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c", "a.txt", "b.txt", "f.txt");
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("e.txt");
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -210,8 +317,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
|
||||
Collection<FileObject> 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
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.Iterator;
|
||||
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 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()
|
||||
.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 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 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<FileObject> 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()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,8 +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;
|
||||
@@ -73,6 +77,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);
|
||||
@@ -127,16 +133,26 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
||||
FileObject parent, String basePath)
|
||||
throws SVNException
|
||||
{
|
||||
Collection<SVNDirEntry> entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null);
|
||||
for (SVNDirEntry entry : entries)
|
||||
{
|
||||
List<SVNDirEntry> entries = new ArrayList<>(svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null));
|
||||
sort(entries, entry -> entry.getKind() == SVNNodeKind.DIR, SVNDirEntry::getName);
|
||||
for (Iterator<SVNDirEntry> iterator = entries.iterator(); resultCount < request.getLimit() + request.getOffset() && iterator.hasNext(); ) {
|
||||
SVNDirEntry entry = iterator.next();
|
||||
FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath);
|
||||
|
||||
parent.addChild(child);
|
||||
if (!child.isDirectory()) {
|
||||
++resultCount;
|
||||
}
|
||||
|
||||
if (child.isDirectory() && request.isRecursive()) {
|
||||
traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath()));
|
||||
}
|
||||
|
||||
if (resultCount > request.getOffset() || (request.getOffset() == 0 && child.isDirectory())) {
|
||||
parent.addChild(child);
|
||||
}
|
||||
}
|
||||
if (resultCount >= request.getLimit() + request.getOffset()) {
|
||||
parent.setTruncated(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,11 +39,12 @@ 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;
|
||||
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,10 +66,17 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
@Test
|
||||
public void testBrowse() {
|
||||
Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest());
|
||||
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
assertNotNull(result);
|
||||
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertThat(foList).extracting("name").containsExactly("c", "a.txt");
|
||||
|
||||
Iterator<FileObject> iterator = foList.iterator();
|
||||
FileObject c = iterator.next();
|
||||
FileObject a = iterator.next();
|
||||
|
||||
assertFalse(a.isDirectory());
|
||||
assertEquals("a.txt", a.getName());
|
||||
@@ -99,24 +107,11 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(2, foList.size());
|
||||
assertThat(foList).extracting("name").containsExactly("d.txt", "e.txt");
|
||||
|
||||
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<FileObject> iterator = foList.iterator();
|
||||
FileObject d = iterator.next();
|
||||
FileObject e = iterator.next();
|
||||
|
||||
assertNotNull(d);
|
||||
assertFalse(d.isDirectory());
|
||||
@@ -140,14 +135,24 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
request.setDisableLastCommit(true);
|
||||
|
||||
Collection<FileObject> foList = getRootFromTip(request);
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
Collection<FileObject> foList1 = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList1);
|
||||
assertFalse(foList1.isEmpty());
|
||||
assertEquals(2, foList1.size());
|
||||
|
||||
Collection<FileObject> 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 +173,102 @@ 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<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertThat(foList).extracting("name").containsExactly("c", "a.txt");
|
||||
assertThat(result.getFile().isTruncated()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOffset() {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
request.setOffset(1);
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertThat(foList).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveLimit() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setLimit(4);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c", "a.txt");
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt", "e.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveLimitInSubDir() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setLimit(1);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c");
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveOffset() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setOffset(1);
|
||||
request.setRecursive(true);
|
||||
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("c", "a.txt");
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("e.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -198,17 +299,4 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||
}
|
||||
|
||||
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) {
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(2, foList.size());
|
||||
|
||||
return foList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ export type File = {
|
||||
length?: number;
|
||||
commitDate?: string;
|
||||
subRepository?: SubRepository; // TODO
|
||||
partialResult: boolean;
|
||||
computationAborted: boolean;
|
||||
partialResult?: boolean;
|
||||
computationAborted?: boolean;
|
||||
truncated?: boolean;
|
||||
_links: Links;
|
||||
_embedded: {
|
||||
children: File[] | null | undefined;
|
||||
|
||||
@@ -128,7 +128,9 @@
|
||||
"noSources": "Keine Sources in diesem Branch gefunden.",
|
||||
"extension": {
|
||||
"notBound": "Keine Erweiterung angebunden."
|
||||
}
|
||||
},
|
||||
"loadMore": "Laden",
|
||||
"moreFilesAvailable": "Es werden nur die ersten {{count}} Dateien angezeigt. Es sind weitere Dateien vorhanden."
|
||||
},
|
||||
"permission": {
|
||||
"title": "Berechtigungen bearbeiten",
|
||||
|
||||
@@ -128,7 +128,9 @@
|
||||
"noSources": "No sources found for this branch.",
|
||||
"extension": {
|
||||
"notBound": "No extension bound."
|
||||
}
|
||||
},
|
||||
"loadMore": "Load",
|
||||
"moreFilesAvailable": "These are just the first {{count}} files. There are more files available."
|
||||
},
|
||||
"permission": {
|
||||
"title": "Edit Permissions",
|
||||
|
||||
@@ -7,26 +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
|
||||
} from "../modules/sources";
|
||||
import FileTreeLeaf from "./FileTreeLeaf";
|
||||
import { Button } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Hunk = {
|
||||
tree: File;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
tree: File;
|
||||
};
|
||||
|
||||
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;
|
||||
updateSources: (hunk: number) => () => void;
|
||||
|
||||
// context props
|
||||
match: any;
|
||||
};
|
||||
|
||||
type State = {
|
||||
stoppableUpdateHandler?: number;
|
||||
stoppableUpdateHandler: number[];
|
||||
};
|
||||
|
||||
const FixedWidthTh = styled.th`
|
||||
@@ -48,44 +62,73 @@ export function findParent(path: string) {
|
||||
class FileTree extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = { stoppableUpdateHandler: [] };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): 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, updateSources } = this.props;
|
||||
hunks?.forEach((hunk, index) => {
|
||||
if (hunk.tree?._embedded?.children && hunk.tree._embedded.children.find(c => c.partialResult)) {
|
||||
const stoppableUpdateHandler = setTimeout(updateSources(index), 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 = () => {
|
||||
this.props.fetchSources(this.props.repository, this.props.revision, this.props.path, this.props.hunks.length);
|
||||
};
|
||||
|
||||
renderTruncatedInfo = () => {
|
||||
const { hunks, t } = this.props;
|
||||
const lastHunk = hunks[hunks.length - 1];
|
||||
const fileCount = hunks
|
||||
.filter(hunk => hunk?.tree?._embedded?.children)
|
||||
.map(hunk => hunk.tree._embedded.children.filter(c => !c.directory).length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
if (lastHunk.tree?.truncated) {
|
||||
return (
|
||||
<Notification type={"info"}>
|
||||
<div className={"columns is-centered"}>
|
||||
<div className={"column"}>{t("sources.moreFilesAvailable", { count: fileCount })}</div>
|
||||
<Button label={t("sources.loadMore")} action={this.loadMore} />
|
||||
</div>
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, loading, tree } = this.props;
|
||||
const { hunks } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (!tree) {
|
||||
if (!hunks || hunks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="panel-block">{this.renderSourcesTable()}</div>;
|
||||
if (hunks[0]?.error) {
|
||||
return <ErrorNotification error={hunks[0].error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel-block">
|
||||
{this.renderSourcesTable()}
|
||||
{this.renderTruncatedInfo()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSourcesTable() {
|
||||
const { tree, revision, path, baseUrl, t } = this.props;
|
||||
const { hunks, revision, path, baseUrl, t } = this.props;
|
||||
|
||||
const files = [];
|
||||
|
||||
@@ -97,53 +140,51 @@ class FileTree extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (tree._embedded && tree._embedded.children) {
|
||||
const children = [...tree._embedded.children].sort(compareFiles);
|
||||
files.push(...children);
|
||||
if (hunks.every(hunk => hunk.loading)) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (files && files.length > 0) {
|
||||
hunks
|
||||
.filter(hunk => !hunk.loading)
|
||||
.forEach(hunk => {
|
||||
if (hunk.tree?._embedded && hunk.tree._embedded.children) {
|
||||
const children = [...hunk.tree._embedded.children];
|
||||
files.push(...children);
|
||||
}
|
||||
});
|
||||
|
||||
const loading = hunks.filter(hunk => hunk.loading).length > 0;
|
||||
|
||||
if (loading || (files && files.length > 0)) {
|
||||
let baseUrlWithRevision = baseUrl;
|
||||
if (revision) {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(revision);
|
||||
} else {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(tree.revision);
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(hunks[0].tree.revision);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<FixedWidthTh />
|
||||
<th>{t("sources.file-tree.name")}</th>
|
||||
<th className="is-hidden-mobile">{t("sources.file-tree.length")}</th>
|
||||
<th className="is-hidden-mobile">{t("sources.file-tree.commitDate")}</th>
|
||||
<th className="is-hidden-touch">{t("sources.file-tree.description")}</th>
|
||||
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<>
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<FixedWidthTh />
|
||||
<th>{t("sources.file-tree.name")}</th>
|
||||
<th className="is-hidden-mobile">{t("sources.file-tree.length")}</th>
|
||||
<th className="is-hidden-mobile">{t("sources.file-tree.commitDate")}</th>
|
||||
<th className="is-hidden-touch">{t("sources.file-tree.description")}</th>
|
||||
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file: any) => (
|
||||
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{hunks[hunks.length - 1].loading && <Loading />}
|
||||
{hunks[hunks.length - 1].error && <ErrorNotification error={hunks[hunks.length - 1].error} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("sources.noSources")}</Notification>;
|
||||
@@ -153,24 +194,36 @@ class FileTree extends React.Component<Props, State> {
|
||||
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 error = getFetchSourcesFailure(state, repository, revision, path, 0);
|
||||
const hunkCount = getHunkCount(state, repository, revision, path);
|
||||
const hunks = [];
|
||||
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,
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
revision,
|
||||
path,
|
||||
loading,
|
||||
error,
|
||||
tree
|
||||
hunks
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ const collection = {
|
||||
length: 176,
|
||||
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
|
||||
subRepository: undefined,
|
||||
truncated: true,
|
||||
partialResult: false,
|
||||
computationAborted: false,
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
@@ -115,17 +118,28 @@ describe("sources fetch", () => {
|
||||
});
|
||||
|
||||
it("should fetch the sources of the repository", () => {
|
||||
fetchMock.getOnce(sourcesUrl, collection);
|
||||
fetchMock.getOnce(sourcesUrl + "?offset=0", collection);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -157,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
|
||||
});
|
||||
|
||||
@@ -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,29 +269,44 @@ 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");
|
||||
|
||||
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, "", "")).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(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +1,73 @@
|
||||
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";
|
||||
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) {
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
for (let i = 0; i < hunk; ++i) {
|
||||
const sources = getSources(state, repository, revision, path, i);
|
||||
if (sources?._embedded.children) {
|
||||
offset += sources._embedded.children.filter(c => !c.directory).length;
|
||||
}
|
||||
}
|
||||
|
||||
return apiClient
|
||||
.get(createUrl(repository, revision, path))
|
||||
.get(createUrl(repository, revision, path, offset))
|
||||
.then(response => response.json())
|
||||
.then((sources: File) => {
|
||||
dispatch(fetchSourcesSuccess(repository, revision, path, 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, err));
|
||||
dispatch(fetchSourcesFailure(repository, revision, path, hunk, err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createUrl(repository: Repository, revision: string, path: string) {
|
||||
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
|
||||
const pathDefined = path ? path : "";
|
||||
return `${base}${encodeURIComponent(revision)}/${pathDefined}`;
|
||||
return `${base}${encodeURIComponent(revision)}/${pathDefined}?offset=${offset}`;
|
||||
}
|
||||
|
||||
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, updatePending: false, sources: {} }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,35 +75,62 @@ export function updateSourcesPending(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
currentSources: any
|
||||
hunk: number,
|
||||
currentSources: File
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_UPDATES_PENDING,
|
||||
payload: { updatePending: true, sources: currentSources },
|
||||
itemId: createItemId(repository, revision, path)
|
||||
payload: { hunk, pending: false, 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 },
|
||||
itemId: createItemId(repository, revision, path)
|
||||
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 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,
|
||||
path: string,
|
||||
hunk: number,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_FAILURE,
|
||||
payload: error,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
payload: { hunk, pending: false, updatePending: false, error },
|
||||
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
|
||||
@@ -98,19 +141,54 @@ export default function reducer(
|
||||
type: "UNKNOWN"
|
||||
}
|
||||
): any {
|
||||
if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_UPDATES_PENDING)) {
|
||||
if (action.itemId && (action.type === FETCH_SOURCES_SUCCESS || action.type === FETCH_SOURCES_FAILURE)) {
|
||||
return {
|
||||
...state,
|
||||
[action.itemId]: action.payload
|
||||
[action.itemId + "hunkCount"]: action.payload.hunk + 1,
|
||||
[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_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,
|
||||
[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
|
||||
|
||||
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 +196,62 @@ 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 = 0
|
||||
): 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 {
|
||||
return state && isPending(state, FETCH_SOURCES, createItemId(repository, revision, path));
|
||||
export function isFetchSourcesPending(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
hunk = 0
|
||||
): boolean {
|
||||
if (state.sources) {
|
||||
return state.sources[createItemId(repository, revision, path, hunk)]?.pending;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (state.sources) {
|
||||
return state.sources[createItemId(repository, revision, path, hunk)]?.updatePending;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getFetchSourcesFailure(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
path: string,
|
||||
hunk = 0
|
||||
): Error | null | undefined {
|
||||
return getFailure(state, FETCH_SOURCES, createItemId(repository, revision, path));
|
||||
if (state.sources) {
|
||||
return state.sources && state.sources[createItemId(repository, revision, path, hunk)]?.error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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 offset, 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) + "?offset=" + (offset + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT)));
|
||||
}
|
||||
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
applyEnrichers(links, embeddedBuilder, namespaceAndName, browserResult, fileObject);
|
||||
|
||||
@@ -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 offset) {
|
||||
FileObjectDto fileObjectDto = fileObjectToDto(browserResult.getFile(), namespaceAndName, browserResult, offset);
|
||||
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 offset);
|
||||
|
||||
@Override
|
||||
void applyEnrichers(Links.Builder links, Embedded.Builder embeddedBuilder, NamespaceAndName namespaceAndName, BrowserResult browserResult, FileObject fileObject) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,11 +9,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;
|
||||
|
||||
@@ -36,38 +37,39 @@ public class SourceRootResource {
|
||||
@Path("")
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Operation(summary = "List of sources", description = "Returns all sources for repository head.", tags = "Repository")
|
||||
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("offset") int offset) throws IOException {
|
||||
return getSource(namespace, name, "/", null, offset);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{revision}")
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Operation(summary = "List of sources by revision", description = "Returns all sources for the given revision.", tags = "Repository")
|
||||
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("offset") int offset) throws IOException {
|
||||
return getSource(namespace, name, "/", revision, offset);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{revision}/{path: .*}")
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Operation(summary = "List of sources by revision in path", description = "Returns all sources for the given revision in a specific path.", tags = "Repository")
|
||||
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("offset") int offset) throws IOException {
|
||||
return getSource(namespace, name, path, revision, offset);
|
||||
}
|
||||
|
||||
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 offset) throws IOException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand();
|
||||
browseCommand.setPath(path);
|
||||
browseCommand.setOffset(offset);
|
||||
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, offset);
|
||||
} else {
|
||||
throw notFound(entity("Source", path).in("Revision", revision).in(namespaceAndName));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user