mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-03 05:09:10 +01:00
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* <p>
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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<FileObject>, 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<FileObject> 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<FileObject> iterator()
|
||||
{
|
||||
Iterator<FileObject> 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<FileObject> 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<FileObject> 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<FileObject> files;
|
||||
|
||||
/** Field description */
|
||||
private String revision;
|
||||
|
||||
/** Field description */
|
||||
private String tag;
|
||||
}
|
||||
|
||||
@@ -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<FileObject> getChildren() {
|
||||
return unmodifiableCollection(children);
|
||||
}
|
||||
|
||||
public void setChildren(List<FileObject> 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<FileObject> children = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -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<FileObject> handler = new PreProcessorHandler<>(fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet, repository);
|
||||
handlePreProcessorForFileObject(handler, result.getFile());
|
||||
}
|
||||
|
||||
private void handlePreProcessorForFileObject(PreProcessorHandler<FileObject> handler, FileObject fileObject) {
|
||||
if (fileObject.isDirectory()) {
|
||||
for (FileObject child : fileObject.getChildren()) {
|
||||
handlePreProcessorForFileObject(handler, child);
|
||||
}
|
||||
}
|
||||
handler.callPreProcessorFactories(fileObject);
|
||||
handler.callPreProcessors(fileObject);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<FileObject> fileObjects = result.getFiles();
|
||||
|
||||
if (fileObjects != null)
|
||||
{
|
||||
Collections.sort(fileObjects, FileObjectNameComparator.instance);
|
||||
result.setFiles(fileObjects);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<SourcesResponse> 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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FileObject> 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<FileObject> 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<String,
|
||||
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,
|
||||
|
||||
@@ -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<FileObject> 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<FileObject> 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<FileObject> 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<FileObject> 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<FileObject> 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<FileObject> foList = root.getChildren();
|
||||
|
||||
List<FileObject> 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<FileObject> foList = root.getChildren();
|
||||
|
||||
List<FileObject> 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<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt", "e.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private GitBrowseCommand createCommand()
|
||||
{
|
||||
private FileObject findFile(Collection<FileObject> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FileObject> execute() throws IOException
|
||||
{
|
||||
cmdAppend("-t");
|
||||
|
||||
List<FileObject> 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<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'));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = '<files>'
|
||||
|
||||
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"])
|
||||
|
||||
@@ -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()
|
||||
@@ -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<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
Collection<FileObject> 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<FileObject> foList = result.getFiles();
|
||||
FileObject c = result.getFile();
|
||||
assertEquals("c", c.getName());
|
||||
Collection<FileObject> foList = c.getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
@@ -128,7 +135,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
request.setDisableLastCommit(true);
|
||||
|
||||
List<FileObject> foList = getRootFromTip(request);
|
||||
Collection<FileObject> foList = getRootFromTip(request);
|
||||
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
|
||||
@@ -147,11 +154,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
FileObject root = result.getFile();
|
||||
Collection<FileObject> 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<FileObject> foList, String name)
|
||||
private FileObject getFileObject(Collection<FileObject> 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<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
|
||||
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
|
||||
BrowserResult result = new HgBrowseCommand(cmdContext,
|
||||
repository).getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
FileObject root = result.getFile();
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
|
||||
@@ -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<SVNDirEntry> entries =
|
||||
svnRepository.getDir(Util.nonNull(path), revisionNumber, null,
|
||||
(Collection) null);
|
||||
List<FileObject> 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<FileObject> children, Collection<SVNDirEntry> entries, String basePath)
|
||||
private void traverse(SVNRepository svnRepository, long revisionNumber, BrowseCommandRequest request,
|
||||
FileObject parent, String basePath)
|
||||
throws SVNException
|
||||
{
|
||||
Collection<SVNDirEntry> 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<SVNDirEntry> 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)
|
||||
{
|
||||
|
||||
@@ -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<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
Collection<FileObject> 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<FileObject> foList = result.getFiles();
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
@@ -135,7 +141,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
request.setDisableLastCommit(true);
|
||||
|
||||
List<FileObject> foList = getRootFromTip(request);
|
||||
Collection<FileObject> foList = getRootFromTip(request);
|
||||
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
|
||||
@@ -151,15 +157,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
Collection<FileObject> 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<FileObject> foList, String name)
|
||||
private FileObject getFileObject(Collection<FileObject> 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<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
|
||||
25
scm-ui-components/packages/ui-types/src/Sources.js
Normal file
25
scm-ui-components/packages/ui-types/src/Sources.js
Normal file
@@ -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[]
|
||||
}
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
30
scm-ui/src/repos/components/RepositoryNavLink.js
Normal file
30
scm-ui/src/repos/components/RepositoryNavLink.js
Normal file
@@ -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<Props> {
|
||||
render() {
|
||||
const { repository, linkName } = this.props;
|
||||
|
||||
if (!repository._links[linkName]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavLink {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default RepositoryNavLink;
|
||||
49
scm-ui/src/repos/components/RepositoryNavLink.test.js
Normal file
49
scm-ui/src/repos/components/RepositoryNavLink.test.js
Normal file
@@ -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(
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
to="/sources"
|
||||
label="Sources"
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the navLink", () => {
|
||||
const repository = {
|
||||
_links: {
|
||||
sources: {
|
||||
href: "/sources"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navLink = mount(
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
to="/sources"
|
||||
label="Sources"
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("Sources");
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Props, State> {
|
||||
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<Props, State> {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,11 +92,12 @@ class BranchRoot extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderBranchSelector = () => {
|
||||
const { repository, branches } = this.props;
|
||||
const { repository, branches, selected } = this.props;
|
||||
if (repository._links.branches) {
|
||||
return (
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selectedBranch={selected}
|
||||
selected={(b: Branch) => {
|
||||
this.branchSelected(b);
|
||||
}}
|
||||
@@ -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<Props> {
|
||||
path={`${url}/changeset/:id`}
|
||||
render={() => <ChangesetView repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/sources`}
|
||||
exact={true}
|
||||
render={() => (
|
||||
<Sources repository={repository} baseUrl={`${url}/sources`} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/sources/:revision/:path*`}
|
||||
render={() => (
|
||||
<Sources repository={repository} baseUrl={`${url}/sources`} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/changesets`}
|
||||
render={() => (
|
||||
@@ -159,11 +174,20 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<Navigation>
|
||||
<Section label={t("repository-root.navigation-label")}>
|
||||
<NavLink to={url} label={t("repository-root.information")} />
|
||||
<NavLink
|
||||
activeOnlyWhenExact={false}
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="changesets"
|
||||
to={`${url}/changesets/`}
|
||||
label={t("repository-root.history")}
|
||||
activeWhenMatch={this.matches}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
to={`${url}/sources`}
|
||||
label={t("repository-root.sources")}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
<PermissionsNavLink
|
||||
|
||||
22
scm-ui/src/repos/sources/components/FileIcon.js
Normal file
22
scm-ui/src/repos/sources/components/FileIcon.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { File } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
file: File
|
||||
};
|
||||
|
||||
class FileIcon extends React.Component<Props> {
|
||||
render() {
|
||||
const { file } = this.props;
|
||||
let icon = "file";
|
||||
if (file.subRepository) {
|
||||
icon = "folder-plus";
|
||||
} else if (file.directory) {
|
||||
icon = "folder";
|
||||
}
|
||||
return <i className={`fa fa-${icon}`} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileIcon;
|
||||
27
scm-ui/src/repos/sources/components/FileSize.js
Normal file
27
scm-ui/src/repos/sources/components/FileSize.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
bytes: number
|
||||
};
|
||||
|
||||
class FileSize extends React.Component<Props> {
|
||||
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 <span>{formattedBytes}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileSize;
|
||||
10
scm-ui/src/repos/sources/components/FileSize.test.js
Normal file
10
scm-ui/src/repos/sources/components/FileSize.test.js
Normal file
@@ -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");
|
||||
});
|
||||
184
scm-ui/src/repos/sources/components/FileTree.js
Normal file
184
scm-ui/src/repos/sources/components/FileTree.js
Normal file
@@ -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<Props> {
|
||||
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 <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
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 (
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={classes.iconColumn} />
|
||||
<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.lastModified")}
|
||||
</th>
|
||||
<th>{t("sources.file-tree.description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<FileTreeLeaf
|
||||
key={file.name}
|
||||
file={file}
|
||||
baseUrl={baseUrlWithRevision}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
revision,
|
||||
path,
|
||||
loading,
|
||||
error,
|
||||
tree
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchSources: (repository: Repository, revision: string, path: string) => {
|
||||
dispatch(fetchSources(repository, revision, path));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(injectSheet(styles)(translate("repos")(FileTree)));
|
||||
12
scm-ui/src/repos/sources/components/FileTree.test.js
Normal file
12
scm-ui/src/repos/sources/components/FileTree.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { findParent } from "./FileTree";
|
||||
|
||||
describe("find parent tests", () => {
|
||||
it("should return the parent path", () => {
|
||||
expect(findParent("src/main/js/")).toBe("src/main");
|
||||
expect(findParent("src/main/js")).toBe("src/main");
|
||||
expect(findParent("src/main")).toBe("src");
|
||||
expect(findParent("src")).toBe("");
|
||||
});
|
||||
});
|
||||
81
scm-ui/src/repos/sources/components/FileTreeLeaf.js
Normal file
81
scm-ui/src/repos/sources/components/FileTreeLeaf.js
Normal file
@@ -0,0 +1,81 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import FileSize from "./FileSize";
|
||||
import FileIcon from "./FileIcon";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { File } from "@scm-manager/ui-types";
|
||||
|
||||
const styles = {
|
||||
iconColumn: {
|
||||
width: "16px"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
file: File,
|
||||
baseUrl: string,
|
||||
|
||||
// context props
|
||||
classes: any
|
||||
};
|
||||
|
||||
export function createLink(base: string, file: File) {
|
||||
let link = base;
|
||||
if (file.path) {
|
||||
let path = file.path;
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
link += "/" + path;
|
||||
}
|
||||
if (!link.endsWith("/")) {
|
||||
link += "/";
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
class FileTreeLeaf extends React.Component<Props> {
|
||||
createLink = (file: File) => {
|
||||
return createLink(this.props.baseUrl, file);
|
||||
};
|
||||
|
||||
createFileIcon = (file: File) => {
|
||||
if (file.directory) {
|
||||
return (
|
||||
<Link to={this.createLink(file)}>
|
||||
<FileIcon file={file} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <FileIcon file={file} />;
|
||||
};
|
||||
|
||||
createFileName = (file: File) => {
|
||||
if (file.directory) {
|
||||
return <Link to={this.createLink(file)}>{file.name}</Link>;
|
||||
}
|
||||
return file.name;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { file, classes } = this.props;
|
||||
|
||||
const fileSize = file.directory ? "" : <FileSize bytes={file.length} />;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
|
||||
<td>{this.createFileName(file)}</td>
|
||||
<td className="is-hidden-mobile">{fileSize}</td>
|
||||
<td className="is-hidden-mobile">
|
||||
<DateFromNow date={file.lastModified} />
|
||||
</td>
|
||||
<td>{file.description}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(FileTreeLeaf);
|
||||
24
scm-ui/src/repos/sources/components/FileTreeLeaf.test.js
Normal file
24
scm-ui/src/repos/sources/components/FileTreeLeaf.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import { createLink } from "./FileTreeLeaf";
|
||||
import type { File } from "@scm-manager/ui-types";
|
||||
|
||||
describe("create link tests", () => {
|
||||
function dir(path: string): File {
|
||||
return {
|
||||
name: "dir",
|
||||
path: path,
|
||||
directory: true
|
||||
};
|
||||
}
|
||||
|
||||
it("should create link", () => {
|
||||
expect(createLink("src", dir("main"))).toBe("src/main/");
|
||||
expect(createLink("src", dir("/main"))).toBe("src/main/");
|
||||
expect(createLink("src", dir("/main/"))).toBe("src/main/");
|
||||
});
|
||||
|
||||
it("should return base url if the directory path is empty", () => {
|
||||
expect(createLink("src", dir(""))).toBe("src/");
|
||||
});
|
||||
});
|
||||
131
scm-ui/src/repos/sources/containers/Sources.js
Normal file
131
scm-ui/src/repos/sources/containers/Sources.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import FileTree from "../components/FileTree";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import BranchSelector from "../../containers/BranchSelector";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../../modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
baseUrl: string,
|
||||
branches: Branch[],
|
||||
revision: string,
|
||||
path: string,
|
||||
|
||||
// dispatch props
|
||||
fetchBranches: Repository => void,
|
||||
|
||||
// Context props
|
||||
history: any,
|
||||
match: any
|
||||
};
|
||||
|
||||
class Sources extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchBranches, repository } = this.props;
|
||||
|
||||
fetchBranches(repository);
|
||||
}
|
||||
|
||||
branchSelected = (branch?: Branch) => {
|
||||
const { baseUrl, history, path } = this.props;
|
||||
|
||||
let url;
|
||||
if (branch) {
|
||||
if (path) {
|
||||
url = `${baseUrl}/${encodeURIComponent(branch.name)}/${path}`;
|
||||
} else {
|
||||
url = `${baseUrl}/${encodeURIComponent(branch.name)}/`;
|
||||
}
|
||||
} else {
|
||||
url = `${baseUrl}/`;
|
||||
}
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, baseUrl, loading, error, revision, path } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderBranchSelector()}
|
||||
<FileTree
|
||||
repository={repository}
|
||||
revision={revision}
|
||||
path={path}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderBranchSelector = () => {
|
||||
const { repository, branches, revision } = this.props;
|
||||
if (repository._links.branches) {
|
||||
return (
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selectedBranch={revision}
|
||||
selected={(b: Branch) => {
|
||||
this.branchSelected(b);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { repository, match } = ownProps;
|
||||
const { revision, path } = match.params;
|
||||
const decodedRevision = revision ? decodeURIComponent(revision) : undefined;
|
||||
|
||||
const loading = isFetchBranchesPending(state, repository);
|
||||
const error = getFetchBranchesFailure(state, repository);
|
||||
const branches = getBranches(state, repository);
|
||||
|
||||
return {
|
||||
repository,
|
||||
revision: decodedRevision,
|
||||
path,
|
||||
loading,
|
||||
error,
|
||||
branches
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repository: Repository) => {
|
||||
dispatch(fetchBranches(repository));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(Sources);
|
||||
141
scm-ui/src/repos/sources/modules/sources.js
Normal file
141
scm-ui/src/repos/sources/modules/sources.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// @flow
|
||||
|
||||
import * as types from "../../../modules/types";
|
||||
import type { Repository, File, Action } 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_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
export function fetchSources(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchSourcesPending(repository, revision, path));
|
||||
return apiClient
|
||||
.get(createUrl(repository, revision, path))
|
||||
.then(response => response.json())
|
||||
.then(sources => {
|
||||
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
||||
})
|
||||
.catch(err => {
|
||||
const error = new Error(`failed to fetch sources: ${err.message}`);
|
||||
dispatch(fetchSourcesFailure(repository, revision, path, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createUrl(repository: Repository, revision: string, path: string) {
|
||||
const base = repository._links.sources.href;
|
||||
if (!revision && !path) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// TODO handle trailing slash
|
||||
const pathDefined = path ? path : "";
|
||||
return `${base}${encodeURIComponent(revision)}/${pathDefined}`;
|
||||
}
|
||||
|
||||
export function fetchSourcesPending(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_PENDING,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesSuccess(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
sources: File
|
||||
) {
|
||||
return {
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
payload: sources,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesFailure(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_FAILURE,
|
||||
payload: error,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
function createItemId(repository: Repository, revision: string, path: string) {
|
||||
const revPart = revision ? revision : "_";
|
||||
const pathPart = path ? path : "";
|
||||
return `${repository.namespace}/${repository.name}/${revPart}/${pathPart}`;
|
||||
}
|
||||
|
||||
// reducer
|
||||
|
||||
export default function reducer(
|
||||
state: any = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): any {
|
||||
if (action.type === FETCH_SOURCES_SUCCESS) {
|
||||
return {
|
||||
[action.itemId]: action.payload,
|
||||
...state
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// selectors
|
||||
|
||||
export function getSources(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): ?File {
|
||||
if (state.sources) {
|
||||
return state.sources[createItemId(repository, revision, path)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isFetchSourcesPending(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): boolean {
|
||||
return isPending(
|
||||
state,
|
||||
FETCH_SOURCES,
|
||||
createItemId(repository, revision, path)
|
||||
);
|
||||
}
|
||||
|
||||
export function getFetchSourcesFailure(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): ?Error {
|
||||
return getFailure(
|
||||
state,
|
||||
FETCH_SOURCES,
|
||||
createItemId(repository, revision, path)
|
||||
);
|
||||
}
|
||||
220
scm-ui/src/repos/sources/modules/sources.test.js
Normal file
220
scm-ui/src/repos/sources/modules/sources.test.js
Normal file
@@ -0,0 +1,220 @@
|
||||
// @flow
|
||||
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {
|
||||
FETCH_SOURCES,
|
||||
FETCH_SOURCES_FAILURE,
|
||||
FETCH_SOURCES_PENDING,
|
||||
FETCH_SOURCES_SUCCESS,
|
||||
fetchSources,
|
||||
getFetchSourcesFailure,
|
||||
isFetchSourcesPending,
|
||||
default as reducer,
|
||||
getSources,
|
||||
fetchSourcesSuccess
|
||||
} from "./sources";
|
||||
|
||||
const sourcesUrl =
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/";
|
||||
|
||||
const repository: Repository = {
|
||||
name: "core",
|
||||
namespace: "scm",
|
||||
type: "git",
|
||||
_links: {
|
||||
sources: {
|
||||
href: sourcesUrl
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collection = {
|
||||
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
files: [
|
||||
{
|
||||
name: "src",
|
||||
path: "src",
|
||||
directory: true,
|
||||
description: null,
|
||||
length: 176,
|
||||
lastModified: null,
|
||||
subRepository: null,
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "package.json",
|
||||
path: "package.json",
|
||||
directory: false,
|
||||
description: "bump version",
|
||||
length: 780,
|
||||
lastModified: "2017-07-31T11:17:19Z",
|
||||
subRepository: null,
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/content/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
|
||||
},
|
||||
history: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/history/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
describe("sources fetch", () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should fetch the sources of the repository", () => {
|
||||
fetchMock.getOnce(sourcesUrl, collection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core/_/" },
|
||||
{
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
itemId: "scm/core/_/",
|
||||
payload: collection
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch the sources of the repository with the given revision and path", () => {
|
||||
fetchMock.getOnce(sourcesUrl + "abc/src", collection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core/abc/src" },
|
||||
{
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
itemId: "scm/core/abc/src",
|
||||
payload: collection
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository, "abc", "src")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_SOURCES_FAILURE on server error", () => {
|
||||
fetchMock.getOnce(sourcesUrl, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository)).then(() => {
|
||||
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].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reducer tests", () => {
|
||||
it("should return unmodified state on unknown action", () => {
|
||||
const state = {};
|
||||
expect(reducer(state)).toBe(state);
|
||||
});
|
||||
|
||||
it("should store the collection, without revision and path", () => {
|
||||
const expectedState = {
|
||||
"scm/core/_/": collection
|
||||
};
|
||||
expect(
|
||||
reducer({}, fetchSourcesSuccess(repository, null, null, collection))
|
||||
).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it("should store the collection, with revision and path", () => {
|
||||
const expectedState = {
|
||||
"scm/core/abc/src/main": collection
|
||||
};
|
||||
expect(
|
||||
reducer(
|
||||
{},
|
||||
fetchSourcesSuccess(repository, "abc", "src/main", collection)
|
||||
)
|
||||
).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
it("should return null", () => {
|
||||
expect(getSources({}, repository)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return the source collection without revision and path", () => {
|
||||
const state = {
|
||||
sources: {
|
||||
"scm/core/_/": collection
|
||||
}
|
||||
};
|
||||
expect(getSources(state, repository)).toBe(collection);
|
||||
});
|
||||
|
||||
it("should return the source collection without revision and path", () => {
|
||||
const state = {
|
||||
sources: {
|
||||
"scm/core/abc/src/main": collection
|
||||
}
|
||||
};
|
||||
expect(getSources(state, repository, "abc", "src/main")).toBe(collection);
|
||||
});
|
||||
|
||||
it("should return true, when fetch sources is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_SOURCES + "/scm/core/_/"]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchSourcesPending(state, repository)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch sources is not pending", () => {
|
||||
expect(isFetchSourcesPending({}, repository)).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
|
||||
}
|
||||
};
|
||||
expect(getFetchSourcesFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch sources did not fail", () => {
|
||||
expect(getFetchSourcesFailure({}, repository)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class BrowserResultDto extends HalRepresentation {
|
||||
private String revision;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
return super.add(links);
|
||||
}
|
||||
|
||||
public void setFiles(List<FileObjectDto> files) {
|
||||
this.withEmbedded("files", files);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BrowserResultToBrowserResultDtoMapper {
|
||||
|
||||
@Inject
|
||||
private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
public BrowserResultDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName, String path) {
|
||||
BrowserResultDto browserResultDto = new BrowserResultDto();
|
||||
|
||||
browserResultDto.setRevision(browserResult.getRevision());
|
||||
|
||||
List<FileObjectDto> fileObjectDtoList = new ArrayList<>();
|
||||
for (FileObject fileObject : browserResult.getFiles()) {
|
||||
fileObjectDtoList.add(mapFileObject(fileObject, namespaceAndName, browserResult.getRevision()));
|
||||
}
|
||||
|
||||
browserResultDto.setFiles(fileObjectDtoList);
|
||||
this.addLinks(browserResult, browserResultDto, namespaceAndName, path);
|
||||
return browserResultDto;
|
||||
}
|
||||
|
||||
private FileObjectDto mapFileObject(FileObject fileObject, NamespaceAndName namespaceAndName, String revision) {
|
||||
return fileObjectToFileObjectDtoMapper.map(fileObject, namespaceAndName, revision);
|
||||
}
|
||||
|
||||
private void addLinks(BrowserResult browserResult, BrowserResultDto dto, NamespaceAndName namespaceAndName, String path) {
|
||||
if (path.equals("/")) {
|
||||
path = "";
|
||||
}
|
||||
if (browserResult.getRevision() == null) {
|
||||
throw new IllegalStateException("missing revision in browser result for repository " + namespaceAndName + " and path " + path);
|
||||
} else {
|
||||
dto.add(Links.linkingTo().self(resourceLinks.source().sourceWithPath(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class BrowserResultToFileObjectDtoMapper {
|
||||
|
||||
private final FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
|
||||
|
||||
@Inject
|
||||
public BrowserResultToFileObjectDtoMapper(FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper) {
|
||||
this.fileObjectToFileObjectDtoMapper = fileObjectToFileObjectDtoMapper;
|
||||
}
|
||||
|
||||
public FileObjectDto map(BrowserResult browserResult, NamespaceAndName namespaceAndName) {
|
||||
FileObjectDto fileObjectDto = fileObjectToFileObjectDtoMapper.map(browserResult.getFile(), namespaceAndName, browserResult.getRevision());
|
||||
fileObjectDto.setRevision( browserResult.getRevision() );
|
||||
return fileObjectDto;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
@@ -7,6 +8,7 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -15,14 +17,26 @@ public class FileObjectDto extends HalRepresentation {
|
||||
private String name;
|
||||
private String path;
|
||||
private boolean directory;
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
private String description;
|
||||
private int length;
|
||||
private long length;
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
private Instant lastModified;
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
private SubRepositoryDto subRepository;
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
private String revision;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
return super.add(links);
|
||||
}
|
||||
|
||||
public void setChildren(List<FileObjectDto> children) {
|
||||
if (!children.isEmpty()) {
|
||||
// prevent empty embedded attribute in json
|
||||
this.withEmbedded("children", children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import sonia.scm.repository.SubRepository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
|
||||
@Mapper
|
||||
|
||||
@@ -4,6 +4,7 @@ import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
class ResourceLinks {
|
||||
|
||||
@@ -16,7 +17,11 @@ class ResourceLinks {
|
||||
|
||||
// we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F'
|
||||
private static String addPath(String sourceWithPath, String path) {
|
||||
return URI.create(sourceWithPath).resolve(path).toASCIIString();
|
||||
try {
|
||||
return new URI(sourceWithPath).resolve(new URI(null, null, path, null)).toASCIIString();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
GroupLinks group() {
|
||||
|
||||
@@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.RepositoryNotFoundException;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
import sonia.scm.repository.api.BrowseCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -21,26 +19,26 @@ import java.io.IOException;
|
||||
public class SourceRootResource {
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper;
|
||||
private final BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper;
|
||||
|
||||
|
||||
@Inject
|
||||
public SourceRootResource(RepositoryServiceFactory serviceFactory, BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper) {
|
||||
public SourceRootResource(RepositoryServiceFactory serviceFactory, BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.browserResultToBrowserResultDtoMapper = browserResultToBrowserResultDtoMapper;
|
||||
this.browserResultToFileObjectDtoMapper = browserResultToFileObjectDtoMapper;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Path("")
|
||||
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RevisionNotFoundException, RepositoryNotFoundException, IOException {
|
||||
public Response getAllWithoutRevision(@PathParam("namespace") String namespace, @PathParam("name") String name) throws NotFoundException, IOException {
|
||||
return getSource(namespace, name, "/", null);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Path("{revision}")
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws RevisionNotFoundException, RepositoryNotFoundException, IOException {
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws NotFoundException, IOException {
|
||||
return getSource(namespace, name, "/", revision);
|
||||
}
|
||||
|
||||
@@ -51,7 +49,7 @@ public class SourceRootResource {
|
||||
return getSource(namespace, name, path, revision);
|
||||
}
|
||||
|
||||
private Response getSource(String namespace, String repoName, String path, String revision) throws IOException, RevisionNotFoundException, RepositoryNotFoundException {
|
||||
private Response getSource(String namespace, String repoName, String path, String revision) throws IOException, NotFoundException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand();
|
||||
@@ -59,10 +57,11 @@ public class SourceRootResource {
|
||||
if (revision != null && !revision.isEmpty()) {
|
||||
browseCommand.setRevision(revision);
|
||||
}
|
||||
browseCommand.setDisableCache(true);
|
||||
BrowserResult browserResult = browseCommand.getBrowserResult();
|
||||
|
||||
if (browserResult != null) {
|
||||
return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName, path)).build();
|
||||
return Response.ok(browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName)).build();
|
||||
} else {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
@@ -8,34 +8,26 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
public class BrowserResultToBrowserResultDtoMapperTest {
|
||||
public class BrowserResultToFileObjectDtoMapperTest {
|
||||
|
||||
private final URI baseUri = URI.create("http://example.com/base/");
|
||||
@SuppressWarnings("unused") // Is injected
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
|
||||
@Mock
|
||||
private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
|
||||
|
||||
@InjectMocks
|
||||
private BrowserResultToBrowserResultDtoMapper mapper;
|
||||
private FileObjectToFileObjectDtoMapperImpl fileObjectToFileObjectDtoMapper;
|
||||
|
||||
private BrowserResultToFileObjectDtoMapper mapper;
|
||||
|
||||
private final Subject subject = mock(Subject.class);
|
||||
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||
@@ -47,6 +39,7 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
||||
@Before
|
||||
public void init() {
|
||||
initMocks(this);
|
||||
mapper = new BrowserResultToFileObjectDtoMapper(fileObjectToFileObjectDtoMapper);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
|
||||
@@ -63,9 +56,6 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
||||
fileObject2.setPath("/path/object/2");
|
||||
fileObject2.setDescription("description of file object 2");
|
||||
fileObject2.setDirectory(true);
|
||||
|
||||
when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString()))
|
||||
.thenReturn(new FileObjectDto());
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -77,7 +67,7 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
||||
public void shouldMapAttributesCorrectly() {
|
||||
BrowserResult browserResult = createBrowserResult();
|
||||
|
||||
BrowserResultDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar"), "path");
|
||||
FileObjectDto dto = mapper.map(browserResult, new NamespaceAndName("foo", "bar"));
|
||||
|
||||
assertEqualAttributes(browserResult, dto);
|
||||
}
|
||||
@@ -87,10 +77,9 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
||||
BrowserResult browserResult = createBrowserResult();
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar");
|
||||
|
||||
BrowserResultDto dto = mapper.map(browserResult, namespaceAndName, "path");
|
||||
FileObjectDto dto = mapper.map(browserResult, namespaceAndName);
|
||||
|
||||
verify(fileObjectToFileObjectDtoMapper).map(fileObject1, namespaceAndName, "Revision");
|
||||
verify(fileObjectToFileObjectDtoMapper).map(fileObject2, namespaceAndName, "Revision");
|
||||
assertThat(dto.getEmbedded().getItemsBy("children")).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -98,28 +87,27 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
||||
BrowserResult browserResult = createBrowserResult();
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar");
|
||||
|
||||
BrowserResultDto dto = mapper.map(browserResult, namespaceAndName, "path");
|
||||
FileObjectDto dto = mapper.map(browserResult, namespaceAndName);
|
||||
|
||||
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).contains("path");
|
||||
}
|
||||
|
||||
private BrowserResult createBrowserResult() {
|
||||
BrowserResult browserResult = new BrowserResult();
|
||||
browserResult.setRevision("Revision");
|
||||
browserResult.setFiles(createFileObjects());
|
||||
|
||||
return browserResult;
|
||||
return new BrowserResult("Revision", createFileObject());
|
||||
}
|
||||
|
||||
private List<FileObject> createFileObjects() {
|
||||
List<FileObject> fileObjects = new ArrayList<>();
|
||||
private FileObject createFileObject() {
|
||||
FileObject file = new FileObject();
|
||||
file.setName("");
|
||||
file.setPath("/path");
|
||||
file.setDirectory(true);
|
||||
|
||||
fileObjects.add(fileObject1);
|
||||
fileObjects.add(fileObject2);
|
||||
return fileObjects;
|
||||
file.addChild(fileObject1);
|
||||
file.addChild(fileObject2);
|
||||
return file;
|
||||
}
|
||||
|
||||
private void assertEqualAttributes(BrowserResult browserResult, BrowserResultDto dto) {
|
||||
private void assertEqualAttributes(BrowserResult browserResult, FileObjectDto dto) {
|
||||
assertThat(dto.getRevision()).isEqualTo(browserResult.getRevision());
|
||||
}
|
||||
|
||||
@@ -173,6 +173,30 @@ public class ResourceLinksTest {
|
||||
assertEquals(BASE_URL + ConfigResource.CONFIG_PATH_V2, url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleSpacesInPaths() {
|
||||
String url = resourceLinks.source().content("space", "name", "rev", "name with spaces");
|
||||
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/content/rev/name%20with%20spaces", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleBackslashInPaths() {
|
||||
String url = resourceLinks.source().content("space", "name", "rev", "name_with_\\");
|
||||
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/content/rev/name_with_%5C", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleNewLineInPaths() {
|
||||
String url = resourceLinks.source().content("space", "name", "rev", "name_with\nnew_line");
|
||||
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/content/rev/name_with%0Anew_line", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldKeepSlashesInInPaths() {
|
||||
String url = resourceLinks.source().content("space", "name", "rev", "some/dir/somewhere");
|
||||
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/content/rev/some/dir/somewhere", url);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void initUriInfo() {
|
||||
initMocks(this);
|
||||
|
||||
@@ -10,11 +10,11 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.RepositoryNotFoundException;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
import sonia.scm.repository.api.BrowseCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -22,12 +22,8 @@ import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||
|
||||
@@ -46,30 +42,25 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
@Mock
|
||||
private BrowseCommandBuilder browseCommandBuilder;
|
||||
|
||||
@Mock
|
||||
private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
|
||||
|
||||
@InjectMocks
|
||||
private BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper;
|
||||
private FileObjectToFileObjectDtoMapperImpl fileObjectToFileObjectDtoMapper;
|
||||
|
||||
private BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper;
|
||||
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() throws Exception {
|
||||
browserResultToFileObjectDtoMapper = new BrowserResultToFileObjectDtoMapper(fileObjectToFileObjectDtoMapper);
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||
when(service.getBrowseCommand()).thenReturn(browseCommandBuilder);
|
||||
|
||||
FileObjectDto dto = new FileObjectDto();
|
||||
dto.setName("name");
|
||||
dto.setLength(1024);
|
||||
|
||||
when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto);
|
||||
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper);
|
||||
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper);
|
||||
super.sourceRootResource = Providers.of(sourceRootResource);
|
||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnSources() throws URISyntaxException, IOException, RevisionNotFoundException {
|
||||
public void shouldReturnSources() throws URISyntaxException, IOException, NotFoundException {
|
||||
BrowserResult result = createBrowserResult();
|
||||
when(browseCommandBuilder.getBrowserResult()).thenReturn(result);
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources");
|
||||
@@ -77,8 +68,9 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
System.out.println(response.getContentAsString());
|
||||
assertThat(response.getContentAsString()).contains("\"revision\":\"revision\"");
|
||||
assertThat(response.getContentAsString()).contains("\"files\":");
|
||||
assertThat(response.getContentAsString()).contains("\"children\":");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,13 +84,11 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, RevisionNotFoundException {
|
||||
BrowserResult browserResult = new BrowserResult();
|
||||
browserResult.setRevision("revision");
|
||||
public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, NotFoundException {
|
||||
FileObject fileObject = new FileObject();
|
||||
fileObject.setName("File Object!");
|
||||
|
||||
browserResult.setFiles(Arrays.asList(fileObject));
|
||||
fileObject.setPath("/");
|
||||
BrowserResult browserResult = new BrowserResult("revision", fileObject);
|
||||
|
||||
when(browseCommandBuilder.getBrowserResult()).thenReturn(browserResult);
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/revision/fileabc");
|
||||
@@ -121,10 +111,15 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
}
|
||||
|
||||
private BrowserResult createBrowserResult() {
|
||||
return new BrowserResult("revision", "tag", "branch", createFileObjects());
|
||||
return new BrowserResult("revision", createFileObject());
|
||||
}
|
||||
|
||||
private List<FileObject> createFileObjects() {
|
||||
private FileObject createFileObject() {
|
||||
FileObject parent = new FileObject();
|
||||
parent.setName("bar");
|
||||
parent.setPath("/foo/bar");
|
||||
parent.setDirectory(true);
|
||||
|
||||
FileObject fileObject1 = new FileObject();
|
||||
fileObject1.setName("FO 1");
|
||||
fileObject1.setDirectory(false);
|
||||
@@ -132,6 +127,7 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
fileObject1.setPath("/foo/bar/fo1");
|
||||
fileObject1.setLength(1024L);
|
||||
fileObject1.setLastModified(0L);
|
||||
parent.addChild(fileObject1);
|
||||
|
||||
FileObject fileObject2 = new FileObject();
|
||||
fileObject2.setName("FO 2");
|
||||
@@ -140,7 +136,8 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
||||
fileObject2.setPath("/foo/bar/fo2");
|
||||
fileObject2.setLength(4096L);
|
||||
fileObject2.setLastModified(1234L);
|
||||
parent.addChild(fileObject2);
|
||||
|
||||
return Arrays.asList(fileObject1, fileObject2);
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user