diff --git a/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java b/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java
index 212ce45f81..17c447eb87 100644
--- a/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java
+++ b/scm-core/src/main/java/sonia/scm/repository/BrowserResult.java
@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
- *
+ *
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
- *
+ *
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,13 +24,11 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
+ *
* http://bitbucket.org/sdorra/scm-manager
- *
*/
-
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
@@ -40,12 +38,8 @@ import com.google.common.base.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
-import java.util.Iterator;
-import java.util.List;
//~--- JDK imports ------------------------------------------------------------
@@ -56,224 +50,56 @@ import java.util.List;
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "browser-result")
-public class BrowserResult implements Iterable, Serializable
-{
+public class BrowserResult implements Serializable {
- /** Field description */
- private static final long serialVersionUID = 2818662048045182761L;
+ private String revision;
+ private FileObject file;
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs ...
- *
- */
- public BrowserResult() {}
-
- /**
- * Constructs ...
- *
- *
- * @param revision
- * @param tag
- * @param branch
- * @param files
- */
- public BrowserResult(String revision, String tag, String branch,
- List files)
- {
- this.revision = revision;
- this.tag = tag;
- this.branch = branch;
- this.files = files;
+ public BrowserResult() {
}
- //~--- methods --------------------------------------------------------------
+ public BrowserResult(String revision, FileObject file) {
+ this.revision = revision;
+ this.file = file;
+ }
+
+ public String getRevision() {
+ return revision;
+ }
+
+ public FileObject getFile() {
+ return file;
+ }
- /**
- * {@inheritDoc}
- *
- *
- * @param obj
- *
- * @return
- */
@Override
- public boolean equals(Object obj)
- {
- if (obj == null)
- {
+ public boolean equals(Object obj) {
+ if (obj == null) {
return false;
}
- if (getClass() != obj.getClass())
- {
+ if (getClass() != obj.getClass()) {
return false;
}
final BrowserResult other = (BrowserResult) obj;
return Objects.equal(revision, other.revision)
- && Objects.equal(tag, other.tag)
- && Objects.equal(branch, other.branch)
- && Objects.equal(files, other.files);
+ && Objects.equal(file, other.file);
}
- /**
- * {@inheritDoc}
- *
- *
- * @return
- */
@Override
- public int hashCode()
- {
- return Objects.hashCode(revision, tag, branch, files);
+ public int hashCode() {
+ return Objects.hashCode(revision, file);
}
- /**
- * Method description
- *
- *
- * @return
- */
+
@Override
- public Iterator iterator()
- {
- Iterator it = null;
-
- if (files != null)
- {
- it = files.iterator();
- }
-
- return it;
- }
-
- /**
- * {@inheritDoc}
- *
- *
- * @return
- */
- @Override
- public String toString()
- {
- //J-
+ public String toString() {
return MoreObjects.toStringHelper(this)
- .add("revision", revision)
- .add("tag", tag)
- .add("branch", branch)
- .add("files", files)
- .toString();
- //J+
+ .add("revision", revision)
+ .add("files", file)
+ .toString();
}
- //~--- get methods ----------------------------------------------------------
- /**
- * Method description
- *
- *
- * @return
- */
- public String getBranch()
- {
- return branch;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- public List getFiles()
- {
- return files;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- public String getRevision()
- {
- return revision;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- public String getTag()
- {
- return tag;
- }
-
- //~--- set methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param branch
- */
- public void setBranch(String branch)
- {
- this.branch = branch;
- }
-
- /**
- * Method description
- *
- *
- * @param files
- */
- public void setFiles(List files)
- {
- this.files = files;
- }
-
- /**
- * Method description
- *
- *
- * @param revision
- */
- public void setRevision(String revision)
- {
- this.revision = revision;
- }
-
- /**
- * Method description
- *
- *
- * @param tag
- */
- public void setTag(String tag)
- {
- this.tag = tag;
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private String branch;
-
- /** Field description */
- @XmlElement(name = "file")
- @XmlElementWrapper(name = "files")
- private List files;
-
- /** Field description */
- private String revision;
-
- /** Field description */
- private String tag;
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java
index 5279921257..7dedebb13a 100644
--- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java
+++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java
@@ -33,10 +33,9 @@
package sonia.scm.repository;
-//~--- non-JDK imports --------------------------------------------------------
-
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
+import com.google.common.base.Strings;
import sonia.scm.LastModifiedAware;
import javax.xml.bind.annotation.XmlAccessType;
@@ -44,8 +43,11 @@ import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
-//~--- JDK imports ------------------------------------------------------------
+import static java.util.Collections.unmodifiableCollection;
/**
* The FileObject represents a file or a directory in a repository.
@@ -181,6 +183,22 @@ public class FileObject implements LastModifiedAware, Serializable
return path;
}
+ /**
+ * Returns the parent path of the file.
+ *
+ * @return parent path
+ */
+ public String getParentPath() {
+ if (Strings.isNullOrEmpty(path)) {
+ return null;
+ }
+ int index = path.lastIndexOf('/');
+ if (index > 0) {
+ return path.substring(0, index);
+ }
+ return "";
+ }
+
/**
* Return sub repository informations or null if the file is not
* sub repository.
@@ -284,6 +302,22 @@ public class FileObject implements LastModifiedAware, Serializable
this.subRepository = subRepository;
}
+ public Collection getChildren() {
+ return unmodifiableCollection(children);
+ }
+
+ public void setChildren(List children) {
+ this.children = new ArrayList<>(children);
+ }
+
+ public void addChild(FileObject child) {
+ this.children.add(child);
+ }
+
+ public boolean hasChildren() {
+ return !children.isEmpty();
+ }
+
//~--- fields ---------------------------------------------------------------
/** file description */
@@ -307,4 +341,6 @@ public class FileObject implements LastModifiedAware, Serializable
/** sub repository informations */
@XmlElement(name = "subrepository")
private SubRepository subRepository;
+
+ private Collection children = new ArrayList<>();
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
index 2a1d9c0340..e64979dde6 100644
--- a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
+++ b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
@@ -161,11 +161,21 @@ public class PreProcessorUtil
{
if (logger.isTraceEnabled())
{
- logger.trace("prepare browser result of repository {} for return",
- repository.getName());
+ logger.trace("prepare browser result of repository {} for return", repository.getName());
}
- handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
+ PreProcessorHandler handler = new PreProcessorHandler<>(fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet, repository);
+ handlePreProcessorForFileObject(handler, result.getFile());
+ }
+
+ private void handlePreProcessorForFileObject(PreProcessorHandler handler, FileObject fileObject) {
+ if (fileObject.isDirectory()) {
+ for (FileObject child : fileObject.getChildren()) {
+ handlePreProcessorForFileObject(handler, child);
+ }
+ }
+ handler.callPreProcessorFactories(fileObject);
+ handler.callPreProcessors(fileObject);
}
/**
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
index fe39aa0a05..d2db6856a7 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java
@@ -38,11 +38,11 @@ package sonia.scm.repository.api;
import com.google.common.base.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
-import sonia.scm.repository.FileObjectNameComparator;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
@@ -52,8 +52,6 @@ import sonia.scm.repository.spi.BrowseCommandRequest;
import java.io.IOException;
import java.io.Serializable;
-import java.util.Collections;
-import java.util.List;
//~--- JDK imports ------------------------------------------------------------
@@ -138,7 +136,7 @@ public final class BrowseCommandBuilder
*
* @throws IOException
*/
- public BrowserResult getBrowserResult() throws IOException, RevisionNotFoundException {
+ public BrowserResult getBrowserResult() throws IOException, NotFoundException {
BrowserResult result = null;
if (disableCache)
@@ -180,14 +178,6 @@ public final class BrowseCommandBuilder
if (!disablePreProcessors && (result != null))
{
preProcessorUtil.prepareForReturn(repository, result);
-
- List fileObjects = result.getFiles();
-
- if (fileObjects != null)
- {
- Collections.sort(fileObjects, FileObjectNameComparator.instance);
- result.setFiles(fileObjects);
- }
}
return result;
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java
index 2c9fff589c..be679f9df1 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/BrowseCommand.java
@@ -35,8 +35,8 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
+import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
-import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
@@ -60,4 +60,5 @@ public interface BrowseCommand
*
* @throws IOException
*/
- BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, RevisionNotFoundException;}
+ BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, NotFoundException;
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/FileObjectTest.java b/scm-core/src/test/java/sonia/scm/repository/FileObjectTest.java
new file mode 100644
index 0000000000..bbd9d0d483
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/FileObjectTest.java
@@ -0,0 +1,32 @@
+package sonia.scm.repository;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class FileObjectTest {
+
+ @Test
+ public void getParentPath() {
+ FileObject file = create("a/b/c");
+ assertEquals("a/b", file.getParentPath());
+ }
+
+ @Test
+ public void getParentPathWithoutParent() {
+ FileObject file = create("a");
+ assertEquals("", file.getParentPath());
+ }
+
+ @Test
+ public void getParentPathOfRoot() {
+ FileObject file = create("");
+ assertNull(file.getParentPath());
+ }
+
+ private FileObject create(String path) {
+ FileObject file = new FileObject();
+ file.setPath(path);
+ return file;
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
index 334274b7b0..66ebc57c90 100644
--- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
@@ -197,7 +197,7 @@ public class RepositoryAccessITCase {
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
- .path("_embedded.files.find{it.name=='a.txt'}._links.self.href");
+ .path("_embedded.children.find{it.name=='a.txt'}._links.self.href");
given()
.when()
@@ -212,7 +212,7 @@ public class RepositoryAccessITCase {
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
- .path("_embedded.files.find{it.name=='subfolder'}._links.self.href");
+ .path("_embedded.children.find{it.name=='subfolder'}._links.self.href");
String selfOfSubfolderUrl = given()
.when()
.get(subfolderSourceUrl)
@@ -227,7 +227,7 @@ public class RepositoryAccessITCase {
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
- .path("_embedded.files[0]._links.self.href");
+ .path("_embedded.children[0]._links.self.href");
given()
.when()
.get(subfolderContentUrl)
diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
index 69c79c37bf..d42a64b98e 100644
--- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java
@@ -2,11 +2,13 @@ package sonia.scm.it.utils;
import io.restassured.RestAssured;
import io.restassured.response.Response;
+import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.user.User;
import sonia.scm.web.VndMediaType;
+import java.net.ConnectException;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -75,10 +77,12 @@ public class ScmRequests {
* @return the response of the GET request using the given link
*/
private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) {
- return applyGETRequestWithQueryParams(response
+ String url = response
.then()
.extract()
- .path(linkPropertyName), params);
+ .path(linkPropertyName);
+ Assert.assertNotNull("no url found for link " + linkPropertyName, url);
+ return applyGETRequestWithQueryParams(url, params);
}
/**
@@ -249,11 +253,11 @@ public class ScmRequests {
}
public ChangesetsResponse requestFileHistory(String fileName) {
- return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"), this);
+ return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.history.href"), this);
}
public SourcesResponse requestSelf(String fileName) {
- return new SourcesResponse<>(applyGETRequestFromLink(response, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"), this);
+ return new SourcesResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.self.href"), this);
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
index f194796bdc..ab1b0ae420 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java
@@ -35,9 +35,9 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
+import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
@@ -50,6 +50,7 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitSubModuleParser;
@@ -103,10 +104,11 @@ public class GitBrowseCommand extends AbstractGitCommand
@Override
@SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request)
- throws IOException, RevisionNotFoundException {
+ throws IOException, NotFoundException {
logger.debug("try to create browse result for {}", request);
BrowserResult result;
+
org.eclipse.jgit.lib.Repository repo = open();
ObjectId revId;
@@ -121,7 +123,7 @@ public class GitBrowseCommand extends AbstractGitCommand
if (revId != null)
{
- result = getResult(repo, request, revId);
+ result = new BrowserResult(revId.getName(), getEntry(repo, request, revId));
}
else
{
@@ -134,8 +136,7 @@ public class GitBrowseCommand extends AbstractGitCommand
logger.warn("coul not find head of repository, empty?");
}
- result = new BrowserResult(Constants.HEAD, null, null,
- Collections.EMPTY_LIST);
+ result = new BrowserResult(Constants.HEAD, createEmtpyRoot());
}
return result;
@@ -143,6 +144,14 @@ public class GitBrowseCommand extends AbstractGitCommand
//~--- methods --------------------------------------------------------------
+ private FileObject createEmtpyRoot() {
+ FileObject fileObject = new FileObject();
+ fileObject.setName("");
+ fileObject.setPath("");
+ fileObject.setDirectory(true);
+ return fileObject;
+ }
+
/**
* Method description
*
@@ -158,68 +167,52 @@ public class GitBrowseCommand extends AbstractGitCommand
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
throws IOException, RevisionNotFoundException {
- FileObject file;
- try
+ FileObject file = new FileObject();
+
+ String path = treeWalk.getPathString();
+
+ file.setName(treeWalk.getNameString());
+ file.setPath(path);
+
+ SubRepository sub = null;
+
+ if (!request.isDisableSubRepositoryDetection())
{
- file = new FileObject();
+ sub = getSubRepository(repo, revId, path);
+ }
- String path = treeWalk.getPathString();
+ if (sub != null)
+ {
+ logger.trace("{} seems to be a sub repository", path);
+ file.setDirectory(true);
+ file.setSubRepository(sub);
+ }
+ else
+ {
+ ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
- file.setName(treeWalk.getNameString());
- file.setPath(path);
+ file.setDirectory(loader.getType() == Constants.OBJ_TREE);
+ file.setLength(loader.getSize());
- SubRepository sub = null;
-
- if (!request.isDisableSubRepositoryDetection())
+ // don't show message and date for directories to improve performance
+ if (!file.isDirectory() &&!request.isDisableLastCommit())
{
- sub = getSubRepository(repo, revId, path);
- }
+ logger.trace("fetch last commit for {} at {}", path, revId.getName());
+ RevCommit commit = getLatestCommit(repo, revId, path);
- if (sub != null)
- {
- logger.trace("{} seems to be a sub repository", path);
- file.setDirectory(true);
- file.setSubRepository(sub);
- }
- else
- {
- ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
-
- file.setDirectory(loader.getType() == Constants.OBJ_TREE);
- file.setLength(loader.getSize());
-
- // don't show message and date for directories to improve performance
- if (!file.isDirectory() &&!request.isDisableLastCommit())
+ if (commit != null)
{
- logger.trace("fetch last commit for {} at {}", path, revId.getName());
-
- RevCommit commit = getLatestCommit(repo, revId, path);
-
- if (commit != null)
- {
- file.setLastModified(GitUtil.getCommitTime(commit));
- file.setDescription(commit.getShortMessage());
- }
- else if (logger.isWarnEnabled())
- {
- logger.warn("could not find latest commit for {} on {}", path,
- revId);
- }
+ file.setLastModified(GitUtil.getCommitTime(commit));
+ file.setDescription(commit.getShortMessage());
+ }
+ else if (logger.isWarnEnabled())
+ {
+ logger.warn("could not find latest commit for {} on {}", path,
+ revId);
}
}
}
- catch (MissingObjectException ex)
- {
- file = null;
- logger.error("could not fetch object for id {}", revId);
-
- if (logger.isTraceEnabled())
- {
- logger.trace("could not fetch object", ex);
- }
- }
-
return file;
}
@@ -265,22 +258,19 @@ public class GitBrowseCommand extends AbstractGitCommand
return result;
}
- private BrowserResult getResult(org.eclipse.jgit.lib.Repository repo,
- BrowseCommandRequest request, ObjectId revId)
- throws IOException, RevisionNotFoundException {
- BrowserResult result = null;
+ private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException, NotFoundException {
RevWalk revWalk = null;
TreeWalk treeWalk = null;
- try
- {
- if (logger.isDebugEnabled())
- {
- logger.debug("load repository browser for revision {}", revId.name());
- }
+ FileObject result;
+
+ try {
+ logger.debug("load repository browser for revision {}", revId.name());
treeWalk = new TreeWalk(repo);
- treeWalk.setRecursive(request.isRecursive());
+ if (!isRootRequest(request)) {
+ treeWalk.setFilter(PathFilter.create(request.getPath()));
+ }
revWalk = new RevWalk(repo);
RevTree tree = revWalk.parseTree(revId);
@@ -291,65 +281,20 @@ public class GitBrowseCommand extends AbstractGitCommand
}
else
{
- logger.error("could not find tree for {}", revId.name());
+ throw new IllegalStateException("could not find tree for " + revId.name());
}
- result = new BrowserResult();
-
- List files = Lists.newArrayList();
-
- String path = request.getPath();
-
- if (Util.isEmpty(path))
- {
- while (treeWalk.next())
- {
- FileObject fo = createFileObject(repo, request, revId, treeWalk);
-
- if (fo != null)
- {
- files.add(fo);
- }
- }
- }
- else
- {
- String[] parts = path.split("/");
- int current = 0;
- int limit = parts.length;
-
- while (treeWalk.next())
- {
- String name = treeWalk.getNameString();
-
- if (current >= limit)
- {
- String p = treeWalk.getPathString();
-
- if (p.split("/").length > limit)
- {
- FileObject fo = createFileObject(repo, request, revId, treeWalk);
-
- if (fo != null)
- {
- files.add(fo);
- }
- }
- }
- else if (name.equalsIgnoreCase(parts[current]))
- {
- current++;
-
- if (!request.isRecursive())
- {
- treeWalk.enterSubtree();
- }
- }
+ if (isRootRequest(request)) {
+ result = createEmtpyRoot();
+ findChildren(result, repo, request, revId, treeWalk);
+ } else {
+ result = findFirstMatch(repo, request, revId, treeWalk);
+ if ( result.isDirectory() ) {
+ treeWalk.enterSubtree();
+ findChildren(result, repo, request, revId, treeWalk);
}
}
- result.setFiles(files);
- result.setRevision(revId.getName());
}
finally
{
@@ -360,6 +305,60 @@ public class GitBrowseCommand extends AbstractGitCommand
return result;
}
+ private boolean isRootRequest(BrowseCommandRequest request) {
+ 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, NotFoundException {
+ List files = Lists.newArrayList();
+ while (treeWalk.next())
+ {
+
+ FileObject fileObject = createFileObject(repo, request, revId, treeWalk);
+ if (!fileObject.getPath().startsWith(parent.getPath())) {
+ parent.setChildren(files);
+ return fileObject;
+ }
+
+ files.add(fileObject);
+
+ if (request.isRecursive() && fileObject.isDirectory()) {
+ treeWalk.enterSubtree();
+ FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk);
+ if (rc != null) {
+ files.add(rc);
+ }
+ }
+ }
+
+ parent.setChildren(files);
+
+ return null;
+ }
+
+ private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
+ BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
+ String[] pathElements = request.getPath().split("/");
+ int currentDepth = 0;
+ int limit = pathElements.length;
+
+ while (treeWalk.next()) {
+ String name = treeWalk.getNameString();
+
+ if (name.equalsIgnoreCase(pathElements[currentDepth])) {
+ currentDepth++;
+
+ if (currentDepth >= limit) {
+ return createFileObject(repo, request, revId, treeWalk);
+ } else {
+ treeWalk.enterSubtree();
+ }
+ }
+ }
+
+ throw new NotFoundException("file", request.getPath());
+ }
+
@SuppressWarnings("unchecked")
private Map getSubRepositories(org.eclipse.jgit.lib.Repository repo,
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
index d71c85a152..92b7ff69a9 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java
@@ -6,13 +6,13 @@
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
@@ -26,152 +26,114 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
- *
*/
-
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.junit.Test;
+import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitConstants;
-import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
-import java.util.List;
+import java.util.Collection;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-//~--- JDK imports ------------------------------------------------------------
-
/**
* Unit tests for {@link GitBrowseCommand}.
- *
+ *
* @author Sebastian Sdorra
*/
-public class GitBrowseCommandTest extends AbstractGitCommandTestBase
-{
-
- /**
- * Test browse command with default branch.
- */
+public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
+
@Test
- public void testDefaultBranch() throws IOException, RevisionNotFoundException {
- // without default branch, the repository head should be used
- BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest());
- assertNotNull(result);
-
- List foList = result.getFiles();
- assertNotNull(foList);
- assertFalse(foList.isEmpty());
- assertEquals(4, foList.size());
-
- assertEquals("a.txt", foList.get(0).getName());
- assertEquals("b.txt", foList.get(1).getName());
- assertEquals("c", foList.get(2).getName());
- assertEquals("f.txt", foList.get(3).getName());
-
- // set default branch and fetch again
- repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
- result = createCommand().getBrowserResult(new BrowseCommandRequest());
- assertNotNull(result);
-
- foList = result.getFiles();
- assertNotNull(foList);
- assertFalse(foList.isEmpty());
- assertEquals(2, foList.size());
-
- assertEquals("a.txt", foList.get(0).getName());
- assertEquals("c", foList.get(1).getName());
+ public void testGetFile() throws IOException, NotFoundException {
+ BrowseCommandRequest request = new BrowseCommandRequest();
+ request.setPath("a.txt");
+ BrowserResult result = createCommand().getBrowserResult(request);
+ FileObject fileObject = result.getFile();
+ assertEquals("a.txt", fileObject.getName());
}
@Test
- public void testBrowse() throws IOException, RevisionNotFoundException {
- BrowserResult result =
- createCommand().getBrowserResult(new BrowseCommandRequest());
-
- assertNotNull(result);
-
- List foList = result.getFiles();
+ public void testDefaultDefaultBranch() throws IOException, NotFoundException {
+ // without default branch, the repository head should be used
+ FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
+ assertNotNull(root);
+ Collection foList = root.getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
- assertEquals(4, foList.size());
- FileObject a = null;
- FileObject c = null;
+ assertThat(foList)
+ .extracting("name")
+ .containsExactly("a.txt", "b.txt", "c", "f.txt");
+ }
- for (FileObject f : foList)
- {
- if ("a.txt".equals(f.getName()))
- {
- a = f;
- }
- else if ("c".equals(f.getName()))
- {
- c = f;
- }
- }
+ @Test
+ public void testExplicitDefaultBranch() throws IOException, NotFoundException {
+ repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
+
+ FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
+ assertNotNull(root);
+
+ Collection foList = root.getChildren();
+ assertThat(foList)
+ .extracting("name")
+ .containsExactly("a.txt", "c");
+ }
+
+ @Test
+ public void testBrowse() throws IOException, NotFoundException {
+ FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
+ assertNotNull(root);
+
+ Collection foList = root.getChildren();
+
+ FileObject a = findFile(foList, "a.txt");
+ FileObject c = findFile(foList, "c");
- assertNotNull(a);
assertFalse(a.isDirectory());
assertEquals("a.txt", a.getName());
assertEquals("a.txt", a.getPath());
assertEquals("added new line for blame", a.getDescription());
assertTrue(a.getLength() > 0);
checkDate(a.getLastModified());
- assertNotNull(c);
+
assertTrue(c.isDirectory());
assertEquals("c", c.getName());
assertEquals("c", c.getPath());
}
@Test
- public void testBrowseSubDirectory() throws IOException, RevisionNotFoundException {
+ public void testBrowseSubDirectory() throws IOException, NotFoundException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("c");
- BrowserResult result = createCommand().getBrowserResult(request);
+ FileObject root = createCommand().getBrowserResult(request).getFile();
- assertNotNull(result);
+ Collection foList = root.getChildren();
- List foList = result.getFiles();
+ assertThat(foList).hasSize(2);
- assertNotNull(foList);
- assertFalse(foList.isEmpty());
- assertEquals(2, foList.size());
+ FileObject d = findFile(foList, "d.txt");
+ FileObject e = findFile(foList, "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;
- }
- }
-
- assertNotNull(d);
assertFalse(d.isDirectory());
assertEquals("d.txt", d.getName());
assertEquals("c/d.txt", d.getPath());
assertEquals("added file d and e in folder c", d.getDescription());
assertTrue(d.getLength() > 0);
checkDate(d.getLastModified());
- assertNotNull(e);
+
assertFalse(e.isDirectory());
assertEquals("e.txt", e.getName());
assertEquals("c/e.txt", e.getPath());
@@ -181,30 +143,35 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase
}
@Test
- public void testRecusive() throws IOException, RevisionNotFoundException {
+ public void testRecursive() throws IOException, NotFoundException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setRecursive(true);
- BrowserResult result = createCommand().getBrowserResult(request);
+ FileObject root = createCommand().getBrowserResult(request).getFile();
- assertNotNull(result);
+ Collection foList = root.getChildren();
- List foList = result.getFiles();
+ assertThat(foList)
+ .extracting("name")
+ .containsExactly("a.txt", "b.txt", "c", "f.txt");
- assertNotNull(foList);
- assertFalse(foList.isEmpty());
- assertEquals(5, foList.size());
+ FileObject c = findFile(foList, "c");
+
+ Collection cChildren = c.getChildren();
+ assertThat(cChildren)
+ .extracting("name")
+ .containsExactly("d.txt", "e.txt");
}
- /**
- * Method description
- *
- *
- * @return
- */
- private GitBrowseCommand createCommand()
- {
+ private FileObject findFile(Collection foList, String name) {
+ return foList.stream()
+ .filter(f -> name.equals(f.getName()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("file " + name + " not found"));
+ }
+
+ private GitBrowseCommand createCommand() {
return new GitBrowseCommand(createContext(), repository);
}
}
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java
index 4e4721ba14..19a8724b69 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBrowseCommand.java
@@ -35,8 +35,10 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
+import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import sonia.scm.repository.BrowserResult;
+import sonia.scm.repository.FileObject;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.javahg.HgFileviewCommand;
@@ -45,6 +47,7 @@ import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/**
+ * Utilizes the mercurial fileview extension in order to support mercurial repository browsing.
*
* @author Sebastian Sdorra
*/
@@ -94,16 +97,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
cmd.disableSubRepositoryDetection();
}
- BrowserResult result = new BrowserResult();
-
- result.setFiles(cmd.execute());
-
- if (!Strings.isNullOrEmpty(request.getRevision())) {
- result.setRevision(request.getRevision());
- } else {
- result.setRevision("tip");
- }
-
- return result;
+ FileObject file = cmd.execute();
+ return new BrowserResult(MoreObjects.firstNonNull(request.getRevision(), "tip"), file);
}
}
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java
index 74695217d2..f351ffa572 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgFileviewCommand.java
@@ -50,35 +50,31 @@ import sonia.scm.repository.SubRepository;
import java.io.IOException;
+import java.util.Deque;
+import java.util.LinkedList;
import java.util.List;
/**
+ * Mercurial command to list files of a repository.
*
* @author Sebastian Sdorra
*/
public class HgFileviewCommand extends AbstractCommand
{
- /**
- * Constructs ...
- *
- *
- * @param repository
- */
- public HgFileviewCommand(Repository repository)
+ private boolean disableLastCommit = false;
+
+ private HgFileviewCommand(Repository repository)
{
super(repository);
}
- //~--- methods --------------------------------------------------------------
-
/**
- * Method description
+ * Create command for the given repository.
*
+ * @param repository repository
*
- * @param repository
- *
- * @return
+ * @return fileview command
*/
public static HgFileviewCommand on(Repository repository)
{
@@ -86,13 +82,11 @@ public class HgFileviewCommand extends AbstractCommand
}
/**
- * Method description
+ * Disable last commit fetching for file objects.
*
- *
- * @return
+ * @return {@code this}
*/
- public HgFileviewCommand disableLastCommit()
- {
+ public HgFileviewCommand disableLastCommit() {
disableLastCommit = true;
cmdAppend("-d");
@@ -100,132 +94,128 @@ public class HgFileviewCommand extends AbstractCommand
}
/**
- * Method description
+ * Disables sub repository detection
*
- *
- * @return
+ * @return {@code this}
*/
- public HgFileviewCommand disableSubRepositoryDetection()
- {
+ public HgFileviewCommand disableSubRepositoryDetection() {
cmdAppend("-s");
return this;
}
/**
- * Method description
+ * Start file object fetching at the given path.
*
*
- * @return
+ * @param path path to start fetching
*
- * @throws IOException
+ * @return {@code this}
*/
- public List execute() throws IOException
- {
- cmdAppend("-t");
-
- List files = Lists.newArrayList();
-
- HgInputStream stream = launchStream();
-
- while (stream.peek() != -1)
- {
- FileObject file = null;
- char type = (char) stream.read();
-
- if (type == 'd')
- {
- file = readDirectory(stream);
- }
- else if (type == 'f')
- {
- file = readFile(stream);
- }
- else if (type == 's')
- {
- file = readSubRepository(stream);
- }
-
- if (file != null)
- {
- files.add(file);
- }
- }
-
- return files;
- }
-
- /**
- * Method description
- *
- *
- * @param path
- *
- * @return
- */
- public HgFileviewCommand path(String path)
- {
+ public HgFileviewCommand path(String path) {
cmdAppend("-p", path);
return this;
}
/**
- * Method description
+ * Fetch file objects recursive.
*
*
- * @return
+ * @return {@code this}
*/
- public HgFileviewCommand recursive()
- {
+ public HgFileviewCommand recursive() {
cmdAppend("-c");
return this;
}
/**
- * Method description
+ * Use given revision for file view.
*
+ * @param revision revision id, hash, tag or branch
*
- * @param revision
- *
- * @return
+ * @return {@code this}
*/
- public HgFileviewCommand rev(String revision)
- {
+ public HgFileviewCommand rev(String revision) {
cmdAppend("-r", revision);
return this;
}
- //~--- get methods ----------------------------------------------------------
-
/**
- * Method description
+ * Executes the mercurial command and parses the output.
*
- *
- * @return
- */
- @Override
- public String getCommandName()
- {
- return HgFileviewExtension.NAME;
- }
-
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param stream
- *
- * @return
+ * @return file object
*
* @throws IOException
*/
- private FileObject readDirectory(HgInputStream stream) throws IOException
+ public FileObject execute() throws IOException
{
+ cmdAppend("-t");
+
+ Deque 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'));
@@ -236,18 +226,7 @@ public class HgFileviewCommand extends AbstractCommand
return directory;
}
- /**
- * Method description
- *
- *
- * @param stream
- *
- * @return
- *
- * @throws IOException
- */
- private FileObject readFile(HgInputStream stream) throws IOException
- {
+ private FileObject readFile(HgInputStream stream) throws IOException {
FileObject file = new FileObject();
String path = removeTrailingSlash(stream.textUpTo('\n'));
@@ -259,8 +238,7 @@ public class HgFileviewCommand extends AbstractCommand
DateTime timestamp = stream.dateTimeUpTo(' ');
String description = stream.textUpTo('\0');
- if (!disableLastCommit)
- {
+ if (!disableLastCommit) {
file.setLastModified(timestamp.getDate().getTime());
file.setDescription(description);
}
@@ -268,18 +246,7 @@ public class HgFileviewCommand extends AbstractCommand
return file;
}
- /**
- * Method description
- *
- *
- * @param stream
- *
- * @return
- *
- * @throws IOException
- */
- private FileObject readSubRepository(HgInputStream stream) throws IOException
- {
+ private FileObject readSubRepository(HgInputStream stream) throws IOException {
FileObject directory = new FileObject();
String path = removeTrailingSlash(stream.textUpTo('\n'));
@@ -292,8 +259,7 @@ public class HgFileviewCommand extends AbstractCommand
SubRepository subRepository = new SubRepository(url);
- if (!Strings.isNullOrEmpty(revision))
- {
+ if (!Strings.isNullOrEmpty(revision)) {
subRepository.setRevision(revision);
}
@@ -302,48 +268,33 @@ public class HgFileviewCommand extends AbstractCommand
return directory;
}
- /**
- * Method description
- *
- *
- * @param path
- *
- * @return
- */
- private String removeTrailingSlash(String path)
- {
- if (path.endsWith("/"))
- {
+ private String removeTrailingSlash(String path) {
+ if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param path
- *
- * @return
- */
- private String getNameFromPath(String path)
- {
+ private String getNameFromPath(String path) {
int index = path.lastIndexOf('/');
- if (index > 0)
- {
+ if (index > 0) {
path = path.substring(index + 1);
}
return path;
}
- //~--- fields ---------------------------------------------------------------
+ /**
+ * Returns the name of the mercurial command.
+ *
+ * @return command name
+ */
+ @Override
+ public String getCommandName()
+ {
+ return HgFileviewExtension.NAME;
+ }
- /** Field description */
- private boolean disableLastCommit = false;
}
diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py
index 518f229011..6aa9bac2f8 100644
--- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py
+++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview.py
@@ -32,61 +32,129 @@
Prints date, size and last message of files.
"""
+
+from collections import defaultdict
from mercurial import cmdutil,util
cmdtable = {}
command = cmdutil.command(cmdtable)
+FILE_MARKER = ''
+
+class File_Collector:
+
+ def __init__(self, recursive = False):
+ self.recursive = recursive
+ self.structure = defaultdict(dict, ((FILE_MARKER, []),))
+
+ def collect(self, paths, path = "", dir_only = False):
+ for p in paths:
+ if p.startswith(path):
+ self.attach(self.extract_name_without_parent(path, p), self.structure, dir_only)
+
+ def attach(self, branch, trunk, dir_only = False):
+ parts = branch.split('/', 1)
+ if len(parts) == 1: # branch is a file
+ if dir_only:
+ trunk[parts[0]] = defaultdict(dict, ((FILE_MARKER, []),))
+ else:
+ trunk[FILE_MARKER].append(parts[0])
+ else:
+ node, others = parts
+ if node not in trunk:
+ trunk[node] = defaultdict(dict, ((FILE_MARKER, []),))
+ if self.recursive:
+ self.attach(others, trunk[node], dir_only)
+
+ def extract_name_without_parent(self, parent, name_with_parent):
+ if len(parent) > 0:
+ name_without_parent = name_with_parent[len(parent):]
+ if name_without_parent.startswith("/"):
+ name_without_parent = name_without_parent[1:]
+ return name_without_parent
+ return name_with_parent
+
+class File_Object:
+ def __init__(self, directory, path):
+ self.directory = directory
+ self.path = path
+ self.children = []
+ self.sub_repository = None
+
+ def get_name(self):
+ parts = self.path.split("/")
+ return parts[len(parts) - 1]
+
+ def get_parent(self):
+ idx = self.path.rfind("/")
+ if idx > 0:
+ return self.path[0:idx]
+ return ""
+
+ def add_child(self, child):
+ self.children.append(child)
+
+ def __getitem__(self, key):
+ return self.children[key]
+
+ def __len__(self):
+ return len(self.children)
+
+ def __repr__(self):
+ result = self.path
+ if self.directory:
+ result += "/"
+ return result
+
+class File_Walker:
+
+ def __init__(self, sub_repositories, visitor):
+ self.visitor = visitor
+ self.sub_repositories = sub_repositories
+
+ def create_file(self, path):
+ return File_Object(False, path)
+
+ def create_directory(self, path):
+ directory = File_Object(True, path)
+ if path in self.sub_repositories:
+ directory.sub_repository = self.sub_repositories[path]
+ return directory
+
+ def visit_file(self, path):
+ file = self.create_file(path)
+ self.visit(file)
+
+ def visit_directory(self, path):
+ file = self.create_directory(path)
+ self.visit(file)
+
+ def visit(self, file):
+ self.visitor.visit(file)
+
+ def create_path(self, parent, path):
+ if len(parent) > 0:
+ return parent + "/" + path
+ return path
+
+ def walk(self, structure, parent = ""):
+ for key, value in structure.iteritems():
+ if key == FILE_MARKER:
+ if value:
+ for v in value:
+ self.visit_file(self.create_path(parent, v))
+ else:
+ self.visit_directory(self.create_path(parent, key))
+ if isinstance(value, dict):
+ self.walk(value, self.create_path(parent, key))
+ else:
+ self.visit_directory(self.create_path(parent, value))
+
class SubRepository:
url = None
revision = None
-def removeTrailingSlash(path):
- if path.endswith('/'):
- path = path[0:-1]
- return path
-
-def appendTrailingSlash(path):
- if not path.endswith('/'):
- path += '/'
- return path
-
-def collectFiles(revCtx, path, files, directories, recursive):
- length = 0
- paths = []
- mf = revCtx.manifest()
- if path is "":
- length = 1
- for f in mf:
- paths.append(f)
- else:
- length = len(path.split('/')) + 1
- directory = path
- if not directory.endswith('/'):
- directory += '/'
-
- for f in mf:
- if f.startswith(directory):
- paths.append(f)
-
- if not recursive:
- for p in paths:
- parts = p.split('/')
- depth = len(parts)
- if depth is length:
- file = revCtx[p]
- files.append(file)
- elif depth > length:
- dirpath = ''
- for i in range(0, length):
- dirpath += parts[i] + '/'
- if not dirpath in directories:
- directories.append(dirpath)
- else:
- for p in paths:
- files.append(revCtx[p])
-
-def createSubRepositoryMap(revCtx):
+def collect_sub_repositories(revCtx):
subrepos = {}
try:
hgsub = revCtx.filectx('.hgsub').data().split('\n')
@@ -98,7 +166,7 @@ def createSubRepositoryMap(revCtx):
subrepos[parts[0].strip()] = subrepo
except Exception:
pass
-
+
try:
hgsubstate = revCtx.filectx('.hgsubstate').data().split('\n')
for line in hgsubstate:
@@ -109,32 +177,77 @@ def createSubRepositoryMap(revCtx):
subrepo.revision = subrev
except Exception:
pass
-
+
return subrepos
-
-def printSubRepository(ui, path, subrepository, transport):
- format = '%s %s %s\n'
- if transport:
- format = 's%s\n%s %s\0'
- ui.write( format % (appendTrailingSlash(path), subrepository.revision, subrepository.url))
-
-def printDirectory(ui, path, transport):
- format = '%s\n'
- if transport:
- format = 'd%s\0'
- ui.write( format % path)
-
-def printFile(ui, repo, file, disableLastCommit, transport):
- date = '0 0'
- description = 'n/a'
- if not disableLastCommit:
- linkrev = repo[file.linkrev()]
- date = '%d %d' % util.parsedate(linkrev.date())
- description = linkrev.description()
- format = '%s %i %s %s\n'
- if transport:
- format = 'f%s\n%i %s %s\0'
- ui.write( format % (file.path(), file.size(), date, description) )
+
+class File_Printer:
+
+ def __init__(self, ui, repo, revCtx, disableLastCommit, transport):
+ self.ui = ui
+ self.repo = repo
+ self.revCtx = revCtx
+ self.disableLastCommit = disableLastCommit
+ self.transport = transport
+
+ def print_directory(self, path):
+ format = '%s/\n'
+ if self.transport:
+ format = 'd%s/\0'
+ self.ui.write( format % path)
+
+ def print_file(self, path):
+ file = self.revCtx[path]
+ date = '0 0'
+ description = 'n/a'
+ if not self.disableLastCommit:
+ linkrev = self.repo[file.linkrev()]
+ date = '%d %d' % util.parsedate(linkrev.date())
+ description = linkrev.description()
+ format = '%s %i %s %s\n'
+ if self.transport:
+ format = 'f%s\n%i %s %s\0'
+ self.ui.write( format % (file.path(), file.size(), date, description) )
+
+ def print_sub_repository(self, path, subrepo):
+ format = '%s/ %s %s\n'
+ if self.transport:
+ format = 's%s/\n%s %s\0'
+ self.ui.write( format % (path, subrepo.revision, subrepo.url))
+
+ def visit(self, file):
+ if file.sub_repository:
+ self.print_sub_repository(file.path, file.sub_repository)
+ elif file.directory:
+ self.print_directory(file.path)
+ else:
+ self.print_file(file.path)
+
+class File_Viewer:
+ def __init__(self, revCtx, visitor):
+ self.revCtx = revCtx
+ self.visitor = visitor
+ self.sub_repositories = {}
+ self.recursive = False
+
+ def remove_ending_slash(self, path):
+ if path.endswith("/"):
+ return path[:-1]
+ return path
+
+ def view(self, path = ""):
+ manifest = self.revCtx.manifest()
+ if len(path) > 0 and path in manifest:
+ self.visitor.visit(File_Object(False, path))
+ else:
+ p = self.remove_ending_slash(path)
+
+ collector = File_Collector(self.recursive)
+ walker = File_Walker(self.sub_repositories, self.visitor)
+
+ self.visitor.visit(File_Object(True, p))
+ collector.collect(manifest, p)
+ collector.collect(self.sub_repositories.keys(), p, True)
+ walker.walk(collector.structure, p)
@command('fileview', [
('r', 'revision', 'tip', 'revision to print'),
@@ -145,23 +258,12 @@ def printFile(ui, repo, file, disableLastCommit, transport):
('t', 'transport', False, 'format the output for command server'),
])
def fileview(ui, repo, **opts):
- files = []
- directories = []
- revision = opts['revision']
- if revision == None:
- revision = 'tip'
- revCtx = repo[revision]
- path = opts['path']
- if path.endswith('/'):
- path = path[0:-1]
- transport = opts['transport']
- collectFiles(revCtx, path, files, directories, opts['recursive'])
- if not opts['disableSubRepositoryDetection']:
- subRepositories = createSubRepositoryMap(revCtx)
- for k, v in subRepositories.iteritems():
- if k.startswith(path):
- printSubRepository(ui, k, v, transport)
- for d in directories:
- printDirectory(ui, d, transport)
- for f in files:
- printFile(ui, repo, f, opts['disableLastCommit'], transport)
+ revCtx = repo[opts["revision"]]
+ subrepos = {}
+ if not opts["disableSubRepositoryDetection"]:
+ subrepos = collect_sub_repositories(revCtx)
+ printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"])
+ viewer = File_Viewer(revCtx, printer)
+ viewer.recursive = opts["recursive"]
+ viewer.sub_repositories = subrepos
+ viewer.view(opts["path"])
diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py
new file mode 100644
index 0000000000..2ce3989d58
--- /dev/null
+++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/hg/ext/fileview_test.py
@@ -0,0 +1,131 @@
+from fileview import File_Viewer, SubRepository
+import unittest
+
+class DummyRevContext():
+
+ def __init__(self, mf):
+ self.mf = mf
+
+ def manifest(self):
+ return self.mf
+
+class File_Object_Collector():
+
+ def __init__(self):
+ self.stack = []
+
+ def __getitem__(self, key):
+ if len(self.stack) == 0 and key == 0:
+ return self.last
+ return self.stack[key]
+
+ def visit(self, file):
+ while len(self.stack) > 0:
+ current = self.stack[-1]
+ if file.get_parent() == current.path:
+ current.add_child(file)
+ break
+ else:
+ self.stack.pop()
+ if file.directory:
+ self.stack.append(file)
+ self.last = file
+
+
+class Test_File_Viewer(unittest.TestCase):
+
+ def test_single_file(self):
+ root = self.collect(["a.txt", "b.txt"], "a.txt")
+ self.assertFile(root, "a.txt")
+
+ def test_simple(self):
+ root = self.collect(["a.txt", "b.txt"])
+ self.assertFile(root[0], "a.txt")
+ self.assertFile(root[1], "b.txt")
+
+ 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.assertDirectory(c, "c")
+ self.assertChildren(c, ["c/d.txt", "c/e.txt", "c/g"])
+ g = c[2]
+ self.assertDirectory(g, "c/g")
+ self.assertChildren(g, ["c/g/h.txt"])
+
+ def test_recursive_with_path(self):
+ root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True)
+ self.assertDirectory(root, "c")
+ self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/g"])
+ g = root[2]
+ self.assertDirectory(g, "c/g")
+ self.assertChildren(g, ["c/g/h.txt"])
+
+ def test_recursive_with_deep_path(self):
+ root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c/g", True)
+ self.assertDirectory(root, "c/g")
+ self.assertChildren(root, ["c/g/h.txt"])
+
+ 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.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.assertEmptyDirectory(f, "c/f")
+
+ def test_non_recursive_with_path_with_ending_slash(self):
+ root = self.collect(["c/d.txt"], "c/")
+ self.assertDirectory(root, "c")
+ self.assertFile(root[0], "c/d.txt")
+
+ def test_with_sub_directory(self):
+ revCtx = DummyRevContext(["a.txt", "b/c.txt"])
+ collector = File_Object_Collector()
+ viewer = File_Viewer(revCtx, collector)
+ sub_repositories = {}
+ sub_repositories["d"] = SubRepository()
+ sub_repositories["d"].url = "d"
+ sub_repositories["d"].revision = "42"
+ viewer.sub_repositories = sub_repositories
+ viewer.view()
+
+ d = collector[0][2]
+ self.assertDirectory(d, "d")
+
+
+ def collect(self, paths, path = "", recursive = False):
+ revCtx = DummyRevContext(paths)
+ collector = File_Object_Collector()
+
+ viewer = File_Viewer(revCtx, collector)
+ viewer.recursive = recursive
+ viewer.view(path)
+
+ return collector[0]
+
+ def assertChildren(self, parent, expectedPaths):
+ 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)
+
+ def assertDirectory(self, file, expectedPath):
+ self.assertEquals(file.path, expectedPath)
+ self.assertTrue(file.directory)
+
+ def assertEmptyDirectory(self, file, expectedPath):
+ self.assertDirectory(file, expectedPath)
+ self.assertTrue(len(file.children) == 0)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java
index c53aa8c607..32b536e69d 100644
--- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBrowseCommandTest.java
@@ -33,14 +33,12 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.junit.Test;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import java.io.IOException;
-import java.util.List;
+import java.util.Collection;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -48,18 +46,25 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-//~--- JDK imports ------------------------------------------------------------
-
/**
*
* @author Sebastian Sdorra
*/
-public class HgBrowseCommandTest extends AbstractHgCommandTestBase
-{
+public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
+
+ @Test
+ public void testBrowseWithFilePath() throws IOException {
+ BrowseCommandRequest request = new BrowseCommandRequest();
+ request.setPath("a.txt");
+ FileObject file = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
+ assertEquals("a.txt", file.getName());
+ assertFalse(file.isDirectory());
+ assertTrue(file.getChildren().isEmpty());
+ }
@Test
public void testBrowse() throws IOException {
- List foList = getRootFromTip(new BrowseCommandRequest());
+ Collection foList = getRootFromTip(new BrowseCommandRequest());
FileObject a = getFileObject(foList, "a.txt");
FileObject c = getFileObject(foList, "c");
@@ -85,7 +90,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
assertNotNull(result);
- List foList = result.getFiles();
+ FileObject c = result.getFile();
+ assertEquals("c", c.getName());
+ Collection foList = c.getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
@@ -128,7 +135,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
request.setDisableLastCommit(true);
- List foList = getRootFromTip(request);
+ Collection foList = getRootFromTip(request);
FileObject a = getFileObject(foList, "a.txt");
@@ -147,11 +154,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
assertNotNull(result);
- List foList = result.getFiles();
+ FileObject root = result.getFile();
+ Collection foList = root.getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
- assertEquals(5, foList.size());
+ assertEquals(4, foList.size());
+
+ FileObject c = getFileObject(foList, "c");
+ assertTrue(c.isDirectory());
+ assertEquals(2, c.getChildren().size());
}
//~--- get methods ----------------------------------------------------------
@@ -165,32 +177,22 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
*
* @return
*/
- private FileObject getFileObject(List foList, String name)
+ private FileObject getFileObject(Collection foList, String name)
{
- FileObject a = null;
-
- for (FileObject f : foList)
- {
- if (name.equals(f.getName()))
- {
- a = f;
-
- break;
- }
- }
-
- assertNotNull(a);
-
- return a;
+ return foList.stream()
+ .filter(f -> name.equals(f.getName()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("file " + name + " not found"));
}
- private List getRootFromTip(BrowseCommandRequest request) throws IOException {
+ private Collection getRootFromTip(BrowseCommandRequest request) throws IOException {
BrowserResult result = new HgBrowseCommand(cmdContext,
repository).getBrowserResult(request);
assertNotNull(result);
- List foList = result.getFiles();
+ FileObject root = result.getFile();
+ Collection foList = root.getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java
index a75adf6b78..43cc1c3c70 100644
--- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java
+++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnBrowseCommand.java
@@ -35,6 +35,7 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
+import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -79,11 +80,11 @@ public class SvnBrowseCommand extends AbstractSvnCommand
@Override
@SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException {
- String path = request.getPath();
+ String path = Strings.nullToEmpty(request.getPath());
long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision());
if (logger.isDebugEnabled()) {
- logger.debug("browser repository {} in path {} at revision {}", repository.getName(), path, revisionNumber);
+ logger.debug("browser repository {} in path \"{}\" at revision {}", repository.getName(), path, revisionNumber);
}
BrowserResult result = null;
@@ -91,34 +92,21 @@ public class SvnBrowseCommand extends AbstractSvnCommand
try
{
SVNRepository svnRepository = open();
- Collection entries =
- svnRepository.getDir(Util.nonNull(path), revisionNumber, null,
- (Collection) null);
- List children = Lists.newArrayList();
- String basePath = createBasePath(path);
-
- if (request.isRecursive())
- {
- browseRecursive(svnRepository, revisionNumber, request, children,
- entries, basePath);
- }
- else
- {
- for (SVNDirEntry entry : entries)
- {
- children.add(createFileObject(request, svnRepository, revisionNumber,
- entry, basePath));
-
- }
- }
if (revisionNumber == -1) {
revisionNumber = svnRepository.getLatestRevision();
}
- result = new BrowserResult();
- result.setRevision(String.valueOf(revisionNumber));
- result.setFiles(children);
+ SVNDirEntry rootEntry = svnRepository.info(path, revisionNumber);
+ FileObject root = createFileObject(request, svnRepository, revisionNumber, rootEntry, path);
+ root.setPath(path);
+
+ if (root.isDirectory()) {
+ traverse(svnRepository, revisionNumber, request, root, createBasePath(path));
+ }
+
+
+ result = new BrowserResult(String.valueOf(revisionNumber), root);
}
catch (SVNException ex)
{
@@ -130,52 +118,24 @@ public class SvnBrowseCommand extends AbstractSvnCommand
//~--- methods --------------------------------------------------------------
- /**
- * Method description
- *
- *
- * @param svnRepository
- * @param revisionNumber
- * @param request
- * @param children
- * @param entries
- * @param basePath
- *
- * @throws SVNException
- */
@SuppressWarnings("unchecked")
- private void browseRecursive(SVNRepository svnRepository,
- long revisionNumber, BrowseCommandRequest request,
- List children, Collection entries, String basePath)
+ private void traverse(SVNRepository svnRepository, long revisionNumber, BrowseCommandRequest request,
+ FileObject parent, String basePath)
throws SVNException
{
+ Collection entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null);
for (SVNDirEntry entry : entries)
{
- FileObject fo = createFileObject(request, svnRepository, revisionNumber,
- entry, basePath);
+ FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath);
- children.add(fo);
+ parent.addChild(child);
- if (fo.isDirectory())
- {
- Collection subEntries =
- svnRepository.getDir(Util.nonNull(fo.getPath()), revisionNumber,
- null, (Collection) null);
-
- browseRecursive(svnRepository, revisionNumber, request, children,
- subEntries, createBasePath(fo.getPath()));
+ if (child.isDirectory() && request.isRecursive()) {
+ traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath()));
}
}
}
- /**
- * Method description
- *
- *
- * @param path
- *
- * @return
- */
private String createBasePath(String path)
{
String basePath = Util.EMPTY_STRING;
@@ -193,20 +153,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
return basePath;
}
- /**
- * Method description
- *
- *
- *
- *
- * @param request
- * @param repository
- * @param revision
- * @param entry
- * @param path
- *
- * @return
- */
private FileObject createFileObject(BrowseCommandRequest request,
SVNRepository repository, long revision, SVNDirEntry entry, String path)
{
@@ -237,15 +183,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
return fileObject;
}
- /**
- * Method description
- *
- *
- * @param repository
- * @param revision
- * @param entry
- * @param fileObject
- */
private void fetchExternalsProperty(SVNRepository repository, long revision,
SVNDirEntry entry, FileObject fileObject)
{
diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java
index c4c658ea7a..bcf5d2ec55 100644
--- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java
+++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnBrowseCommandTest.java
@@ -33,15 +33,13 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.junit.Test;
import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
-import java.util.List;
+import java.util.Collection;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -49,8 +47,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-//~--- JDK imports ------------------------------------------------------------
-
/**
*
* @author Sebastian Sdorra
@@ -58,9 +54,19 @@ import static org.junit.Assert.assertTrue;
public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
{
+ @Test
+ public void testBrowseWithFilePath() throws RevisionNotFoundException {
+ BrowseCommandRequest request = new BrowseCommandRequest();
+ request.setPath("a.txt");
+ FileObject file = createCommand().getBrowserResult(request).getFile();
+ assertEquals("a.txt", file.getName());
+ assertFalse(file.isDirectory());
+ assertTrue(file.getChildren().isEmpty());
+ }
+
@Test
public void testBrowse() throws RevisionNotFoundException {
- List foList = getRootFromTip(new BrowseCommandRequest());
+ Collection foList = getRootFromTip(new BrowseCommandRequest());
FileObject a = getFileObject(foList, "a.txt");
FileObject c = getFileObject(foList, "c");
@@ -92,7 +98,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
assertNotNull(result);
- List foList = result.getFiles();
+ Collection foList = result.getFile().getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
@@ -135,7 +141,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
request.setDisableLastCommit(true);
- List foList = getRootFromTip(request);
+ Collection foList = getRootFromTip(request);
FileObject a = getFileObject(foList, "a.txt");
@@ -151,15 +157,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
assertNotNull(result);
- List foList = result.getFiles();
+ Collection foList = result.getFile().getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
- assertEquals(4, foList.size());
-
- for ( FileObject fo : foList ){
- System.out.println(fo);
- }
+ assertEquals(2, foList.size());
+
+ FileObject c = getFileObject(foList, "c");
+ assertEquals("c", c.getName());
+ assertTrue(c.isDirectory());
+ assertEquals(2, c.getChildren().size());
}
/**
@@ -184,31 +191,20 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
*
* @return
*/
- private FileObject getFileObject(List foList, String name)
+ private FileObject getFileObject(Collection foList, String name)
{
- FileObject a = null;
-
- for (FileObject f : foList)
- {
- if (name.equals(f.getName()))
- {
- a = f;
-
- break;
- }
- }
-
- assertNotNull(a);
-
- return a;
+ return foList.stream()
+ .filter(f -> name.equals(f.getName()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("file " + name + " not found"));
}
- private List getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
+ private Collection getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
BrowserResult result = createCommand().getBrowserResult(request);
assertNotNull(result);
- List foList = result.getFiles();
+ Collection foList = result.getFile().getChildren();
assertNotNull(foList);
assertFalse(foList.isEmpty());
diff --git a/scm-ui-components/packages/ui-types/src/Sources.js b/scm-ui-components/packages/ui-types/src/Sources.js
new file mode 100644
index 0000000000..c8b3fafe0c
--- /dev/null
+++ b/scm-ui-components/packages/ui-types/src/Sources.js
@@ -0,0 +1,25 @@
+// @flow
+
+import type { Collection, Links } from "./hal";
+
+// TODO ?? check ?? links
+export type SubRepository = {
+ repositoryUrl: string,
+ browserUrl: string,
+ revision: string
+};
+
+export type File = {
+ name: string,
+ path: string,
+ directory: boolean,
+ description?: string,
+ revision: string,
+ length: number,
+ lastModified?: string,
+ subRepository?: SubRepository, // TODO
+ _links: Links,
+ _embedded: {
+ children: File[]
+ }
+};
diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js
index 98a6ead283..948b98ffe5 100644
--- a/scm-ui-components/packages/ui-types/src/index.js
+++ b/scm-ui-components/packages/ui-types/src/index.js
@@ -18,3 +18,5 @@ export type { Tag } from "./Tags";
export type { Config } from "./Config";
export type { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions";
+
+export type { SubRepository, File } from "./Sources";
diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json
index c9dcc84815..44ba35ddae 100644
--- a/scm-ui/public/locales/en/repos.json
+++ b/scm-ui/public/locales/en/repos.json
@@ -24,7 +24,8 @@
"navigation-label": "Navigation",
"history": "Commits",
"information": "Information",
- "permissions": "Permissions"
+ "permissions": "Permissions",
+ "sources": "Sources"
},
"create": {
"title": "Create Repository",
@@ -45,6 +46,14 @@
"cancel": "No"
}
},
+ "sources": {
+ "file-tree": {
+ "name": "Name",
+ "length": "Length",
+ "lastModified": "Last modified",
+ "description": "Description"
+ }
+ },
"changesets": {
"error-title": "Error",
"error-subtitle": "Could not fetch changesets",
diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js
index 710ade20e5..8d3e016c80 100644
--- a/scm-ui/src/createReduxStore.js
+++ b/scm-ui/src/createReduxStore.js
@@ -8,6 +8,7 @@ import users from "./users/modules/users";
import repos from "./repos/modules/repos";
import repositoryTypes from "./repos/modules/repositoryTypes";
import changesets from "./repos/modules/changesets";
+import sources from "./repos/sources/modules/sources";
import groups from "./groups/modules/groups";
import auth from "./modules/auth";
import pending from "./modules/pending";
@@ -34,7 +35,8 @@ function createReduxStore(history: BrowserHistory) {
permissions,
groups,
auth,
- config
+ config,
+ sources
});
return createStore(
diff --git a/scm-ui/src/repos/components/RepositoryNavLink.js b/scm-ui/src/repos/components/RepositoryNavLink.js
new file mode 100644
index 0000000000..b4cf7774af
--- /dev/null
+++ b/scm-ui/src/repos/components/RepositoryNavLink.js
@@ -0,0 +1,30 @@
+//@flow
+import React from "react";
+import type { Repository } from "@scm-manager/ui-types";
+import { NavLink } from "@scm-manager/ui-components";
+
+type Props = {
+ repository: Repository,
+ to: string,
+ label: string,
+ linkName: string,
+ activeWhenMatch?: (route: any) => boolean,
+ activeOnlyWhenExact: boolean
+};
+
+/**
+ * Component renders only if the repository contains the link with the given name.
+ */
+class RepositoryNavLink extends React.Component {
+ render() {
+ const { repository, linkName } = this.props;
+
+ if (!repository._links[linkName]) {
+ return null;
+ }
+
+ return ;
+ }
+}
+
+export default RepositoryNavLink;
diff --git a/scm-ui/src/repos/components/RepositoryNavLink.test.js b/scm-ui/src/repos/components/RepositoryNavLink.test.js
new file mode 100644
index 0000000000..0d93cb7c4d
--- /dev/null
+++ b/scm-ui/src/repos/components/RepositoryNavLink.test.js
@@ -0,0 +1,49 @@
+// @flow
+import React from "react";
+import { shallow, mount } from "enzyme";
+import "../../tests/enzyme";
+import "../../tests/i18n";
+import ReactRouterEnzymeContext from "react-router-enzyme-context";
+import RepositoryNavLink from "./RepositoryNavLink";
+
+describe("RepositoryNavLink", () => {
+ const options = new ReactRouterEnzymeContext();
+
+ it("should render nothing, if the sources link is missing", () => {
+ const repository = {
+ _links: {}
+ };
+
+ const navLink = shallow(
+ ,
+ options.get()
+ );
+ expect(navLink.text()).toBe("");
+ });
+
+ it("should render the navLink", () => {
+ const repository = {
+ _links: {
+ sources: {
+ href: "/sources"
+ }
+ }
+ };
+
+ const navLink = mount(
+ ,
+ options.get()
+ );
+ expect(navLink.text()).toBe("Sources");
+ });
+});
diff --git a/scm-ui/src/repos/components/changesets/changesets.js b/scm-ui/src/repos/components/changesets/changesets.js
index 9d4821e5ec..f61a89c74b 100644
--- a/scm-ui/src/repos/components/changesets/changesets.js
+++ b/scm-ui/src/repos/components/changesets/changesets.js
@@ -4,17 +4,18 @@ export type Description = {
message: string
};
-export function parseDescription(description: string): Description {
- const lineBreak = description.indexOf("\n");
+export function parseDescription(description?: string): Description {
+ const desc = description ? description : "";
+ const lineBreak = desc.indexOf("\n");
let title;
let message = "";
if (lineBreak > 0) {
- title = description.substring(0, lineBreak);
- message = description.substring(lineBreak + 1);
+ title = desc.substring(0, lineBreak);
+ message = desc.substring(lineBreak + 1);
} else {
- title = description;
+ title = desc;
}
return {
diff --git a/scm-ui/src/repos/components/changesets/changesets.test.js b/scm-ui/src/repos/components/changesets/changesets.test.js
index cd3788e9fb..ea92bcead3 100644
--- a/scm-ui/src/repos/components/changesets/changesets.test.js
+++ b/scm-ui/src/repos/components/changesets/changesets.test.js
@@ -13,4 +13,10 @@ describe("parseDescription tests", () => {
const desc = parseDescription("Hello Trillian");
expect(desc.title).toBe("Hello Trillian");
});
+
+ it("should return an empty description for undefined", () => {
+ const desc = parseDescription();
+ expect(desc.title).toBe("");
+ expect(desc.message).toBe("");
+ });
});
diff --git a/scm-ui/src/repos/containers/BranchSelector.js b/scm-ui/src/repos/containers/BranchSelector.js
index 2d8c817542..2183e13b69 100644
--- a/scm-ui/src/repos/containers/BranchSelector.js
+++ b/scm-ui/src/repos/containers/BranchSelector.js
@@ -17,6 +17,7 @@ const styles = {
type Props = {
branches: Branch[], // TODO: Use generics?
selected: (branch?: Branch) => void,
+ selectedBranch: string,
// context props
classes: Object,
@@ -31,6 +32,12 @@ class BranchSelector extends React.Component {
this.state = {};
}
+ componentDidMount() {
+ this.props.branches
+ .filter(branch => branch.name === this.props.selectedBranch)
+ .forEach(branch => this.setState({ selectedBranch: branch }));
+ }
+
render() {
const { branches, classes, t } = this.props;
@@ -60,6 +67,8 @@ class BranchSelector extends React.Component {
);
+ } else {
+ return null;
}
}
diff --git a/scm-ui/src/repos/containers/BranchRoot.js b/scm-ui/src/repos/containers/ChangesetsRoot.js
similarity index 97%
rename from scm-ui/src/repos/containers/BranchRoot.js
rename to scm-ui/src/repos/containers/ChangesetsRoot.js
index c60dc7281a..1f3f0c1e3b 100644
--- a/scm-ui/src/repos/containers/BranchRoot.js
+++ b/scm-ui/src/repos/containers/ChangesetsRoot.js
@@ -92,11 +92,12 @@ class BranchRoot extends React.Component {
}
renderBranchSelector = () => {
- const { repository, branches } = this.props;
+ const { repository, branches, selected } = this.props;
if (repository._links.branches) {
return (
{
this.branchSelected(b);
}}
diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js
index 1732170c79..9475612765 100644
--- a/scm-ui/src/repos/containers/RepositoryRoot.js
+++ b/scm-ui/src/repos/containers/RepositoryRoot.js
@@ -29,9 +29,11 @@ import Permissions from "../permissions/containers/Permissions";
import type { History } from "history";
import EditNavLink from "../components/EditNavLink";
-import BranchRoot from "./BranchRoot";
+import BranchRoot from "./ChangesetsRoot";
import ChangesetView from "./ChangesetView";
import PermissionsNavLink from "../components/PermissionsNavLink";
+import Sources from "../sources/containers/Sources";
+import RepositoryNavLink from "../components/RepositoryNavLink";
type Props = {
namespace: string,
@@ -133,6 +135,19 @@ class RepositoryRoot extends React.Component {
path={`${url}/changeset/:id`}
render={() => }
/>
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
(
@@ -159,11 +174,20 @@ class RepositoryRoot extends React.Component {
-
+ {
+ render() {
+ const { file } = this.props;
+ let icon = "file";
+ if (file.subRepository) {
+ icon = "folder-plus";
+ } else if (file.directory) {
+ icon = "folder";
+ }
+ return ;
+ }
+}
+
+export default FileIcon;
diff --git a/scm-ui/src/repos/sources/components/FileSize.js b/scm-ui/src/repos/sources/components/FileSize.js
new file mode 100644
index 0000000000..c7d966d901
--- /dev/null
+++ b/scm-ui/src/repos/sources/components/FileSize.js
@@ -0,0 +1,27 @@
+// @flow
+import React from "react";
+
+type Props = {
+ bytes: number
+};
+
+class FileSize extends React.Component {
+ static format(bytes: number) {
+ if (!bytes) {
+ return "0 B";
+ }
+
+ const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+
+ const size = i === 0 ? bytes : (bytes / 1024 ** i).toFixed(2);
+ return `${size} ${units[i]}`;
+ }
+
+ render() {
+ const formattedBytes = FileSize.format(this.props.bytes);
+ return {formattedBytes};
+ }
+}
+
+export default FileSize;
diff --git a/scm-ui/src/repos/sources/components/FileSize.test.js b/scm-ui/src/repos/sources/components/FileSize.test.js
new file mode 100644
index 0000000000..8ecb53e1bb
--- /dev/null
+++ b/scm-ui/src/repos/sources/components/FileSize.test.js
@@ -0,0 +1,10 @@
+import FileSize from "./FileSize";
+
+it("should format bytes", () => {
+ expect(FileSize.format(0)).toBe("0 B");
+ expect(FileSize.format(160)).toBe("160 B");
+ expect(FileSize.format(6304)).toBe("6.16 K");
+ expect(FileSize.format(28792588)).toBe("27.46 M");
+ expect(FileSize.format(1369510189)).toBe("1.28 G");
+ expect(FileSize.format(42949672960)).toBe("40.00 G");
+});
diff --git a/scm-ui/src/repos/sources/components/FileTree.js b/scm-ui/src/repos/sources/components/FileTree.js
new file mode 100644
index 0000000000..02aa22f942
--- /dev/null
+++ b/scm-ui/src/repos/sources/components/FileTree.js
@@ -0,0 +1,184 @@
+//@flow
+import React from "react";
+import { translate } from "react-i18next";
+import { connect } from "react-redux";
+import injectSheet from "react-jss";
+import FileTreeLeaf from "./FileTreeLeaf";
+import type { Repository, File } from "@scm-manager/ui-types";
+import { ErrorNotification, Loading } from "@scm-manager/ui-components";
+import {
+ fetchSources,
+ getFetchSourcesFailure,
+ isFetchSourcesPending,
+ getSources
+} from "../modules/sources";
+import { withRouter } from "react-router-dom";
+import { compose } from "redux";
+
+const styles = {
+ iconColumn: {
+ width: "16px"
+ }
+};
+
+type Props = {
+ loading: boolean,
+ error: Error,
+ tree: File,
+ repository: Repository,
+ revision: string,
+ path: string,
+ baseUrl: string,
+ fetchSources: (Repository, string, string) => void,
+ // context props
+ classes: any,
+ t: string => string,
+ match: any
+};
+
+export function findParent(path: string) {
+ if (path.endsWith("/")) {
+ path = path.substring(0, path.length - 1);
+ }
+
+ const index = path.lastIndexOf("/");
+ if (index > 0) {
+ return path.substring(0, index);
+ }
+ return "";
+}
+
+class FileTree extends React.Component {
+ componentDidMount() {
+ const { fetchSources, repository, revision, path } = this.props;
+
+ fetchSources(repository, revision, path);
+ }
+
+ componentDidUpdate(prevProps) {
+ const { fetchSources, repository, revision, path } = this.props;
+ if (prevProps.revision !== revision || prevProps.path !== path) {
+ fetchSources(repository, revision, path);
+ }
+ }
+
+ render() {
+ const {
+ error,
+ loading,
+ tree,
+ revision,
+ path,
+ baseUrl,
+ classes,
+ t
+ } = this.props;
+
+ 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 (error) {
+ return ;
+ }
+
+ if (loading) {
+ return ;
+ }
+ if (!tree) {
+ return null;
+ }
+
+ const files = [];
+
+ if (path) {
+ files.push({
+ name: "..",
+ path: findParent(path),
+ directory: true
+ });
+ }
+
+ if (tree._embedded) {
+ files.push(...tree._embedded.children.sort(compareFiles));
+ }
+
+ let baseUrlWithRevision = baseUrl;
+ if (revision) {
+ baseUrlWithRevision += "/" + encodeURIComponent(revision);
+ } else {
+ baseUrlWithRevision += "/" + encodeURIComponent(tree.revision);
+ }
+
+ return (
+