mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 11:35:57 +01:00
merge
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
* Copyright (c) 2010, Sebastian Sdorra
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
* <p>
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
* modification, are permitted provided that the following conditions are met:
|
* modification, are permitted provided that the following conditions are met:
|
||||||
*
|
* <p>
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
* 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,
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||||
* contributors may be used to endorse or promote products derived from this
|
* contributors may be used to endorse or promote products derived from this
|
||||||
* software without specific prior written permission.
|
* software without specific prior written permission.
|
||||||
*
|
* <p>
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
* 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
|
* 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
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
* <p>
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.repository;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
@@ -40,12 +38,8 @@ import com.google.common.base.Objects;
|
|||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
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 javax.xml.bind.annotation.XmlRootElement;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -56,224 +50,56 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@XmlRootElement(name = "browser-result")
|
@XmlRootElement(name = "browser-result")
|
||||||
public class BrowserResult implements Iterable<FileObject>, Serializable
|
public class BrowserResult implements Serializable {
|
||||||
{
|
|
||||||
|
|
||||||
/** Field description */
|
private String revision;
|
||||||
private static final long serialVersionUID = 2818662048045182761L;
|
private FileObject file;
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
public BrowserResult() {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- 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
|
@Override
|
||||||
public boolean equals(Object obj)
|
public boolean equals(Object obj) {
|
||||||
{
|
if (obj == null) {
|
||||||
if (obj == null)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getClass() != obj.getClass())
|
if (getClass() != obj.getClass()) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final BrowserResult other = (BrowserResult) obj;
|
final BrowserResult other = (BrowserResult) obj;
|
||||||
|
|
||||||
return Objects.equal(revision, other.revision)
|
return Objects.equal(revision, other.revision)
|
||||||
&& Objects.equal(tag, other.tag)
|
&& Objects.equal(file, other.file);
|
||||||
&& Objects.equal(branch, other.branch)
|
|
||||||
&& Objects.equal(files, other.files);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode()
|
public int hashCode() {
|
||||||
{
|
return Objects.hashCode(revision, file);
|
||||||
return Objects.hashCode(revision, tag, branch, files);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Iterator<FileObject> iterator()
|
public String toString() {
|
||||||
{
|
|
||||||
Iterator<FileObject> it = null;
|
|
||||||
|
|
||||||
if (files != null)
|
|
||||||
{
|
|
||||||
it = files.iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
return it;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
//J-
|
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
.add("revision", revision)
|
.add("revision", revision)
|
||||||
.add("tag", tag)
|
.add("files", file)
|
||||||
.add("branch", branch)
|
|
||||||
.add("files", files)
|
|
||||||
.toString();
|
.toString();
|
||||||
//J+
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- 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;
|
package sonia.scm.repository;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import sonia.scm.LastModifiedAware;
|
import sonia.scm.LastModifiedAware;
|
||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
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.XmlElement;
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
import java.io.Serializable;
|
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.
|
* The FileObject represents a file or a directory in a repository.
|
||||||
@@ -181,6 +183,22 @@ public class FileObject implements LastModifiedAware, Serializable
|
|||||||
return path;
|
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
|
* Return sub repository informations or null if the file is not
|
||||||
* sub repository.
|
* sub repository.
|
||||||
@@ -284,6 +302,22 @@ public class FileObject implements LastModifiedAware, Serializable
|
|||||||
this.subRepository = subRepository;
|
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 ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** file description */
|
/** file description */
|
||||||
@@ -307,4 +341,6 @@ public class FileObject implements LastModifiedAware, Serializable
|
|||||||
/** sub repository informations */
|
/** sub repository informations */
|
||||||
@XmlElement(name = "subrepository")
|
@XmlElement(name = "subrepository")
|
||||||
private SubRepository subRepository;
|
private SubRepository subRepository;
|
||||||
|
|
||||||
|
private Collection<FileObject> children = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,11 +161,21 @@ public class PreProcessorUtil
|
|||||||
{
|
{
|
||||||
if (logger.isTraceEnabled())
|
if (logger.isTraceEnabled())
|
||||||
{
|
{
|
||||||
logger.trace("prepare browser result of repository {} for return",
|
logger.trace("prepare browser result of repository {} for return", repository.getName());
|
||||||
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 com.google.common.base.Objects;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.cache.Cache;
|
import sonia.scm.cache.Cache;
|
||||||
import sonia.scm.cache.CacheManager;
|
import sonia.scm.cache.CacheManager;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.FileObjectNameComparator;
|
|
||||||
import sonia.scm.repository.PreProcessorUtil;
|
import sonia.scm.repository.PreProcessorUtil;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryCacheKey;
|
import sonia.scm.repository.RepositoryCacheKey;
|
||||||
@@ -52,8 +52,6 @@ import sonia.scm.repository.spi.BrowseCommandRequest;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -138,7 +136,7 @@ public final class BrowseCommandBuilder
|
|||||||
*
|
*
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public BrowserResult getBrowserResult() throws IOException, RevisionNotFoundException {
|
public BrowserResult getBrowserResult() throws IOException, NotFoundException {
|
||||||
BrowserResult result = null;
|
BrowserResult result = null;
|
||||||
|
|
||||||
if (disableCache)
|
if (disableCache)
|
||||||
@@ -180,14 +178,6 @@ public final class BrowseCommandBuilder
|
|||||||
if (!disablePreProcessors && (result != null))
|
if (!disablePreProcessors && (result != null))
|
||||||
{
|
{
|
||||||
preProcessorUtil.prepareForReturn(repository, result);
|
preProcessorUtil.prepareForReturn(repository, result);
|
||||||
|
|
||||||
List<FileObject> fileObjects = result.getFiles();
|
|
||||||
|
|
||||||
if (fileObjects != null)
|
|
||||||
{
|
|
||||||
Collections.sort(fileObjects, FileObjectNameComparator.instance);
|
|
||||||
result.setFiles(fileObjects);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.RevisionNotFoundException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@@ -60,4 +60,5 @@ public interface BrowseCommand
|
|||||||
*
|
*
|
||||||
* @throws IOException
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
scm-it/src/test/java/sonia/scm/it/I18nServletITCase.java
Normal file
19
scm-it/src/test/java/sonia/scm/it/I18nServletITCase.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package sonia.scm.it;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import sonia.scm.it.utils.ScmRequests;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class I18nServletITCase {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetCollectedPluginTranslations() {
|
||||||
|
ScmRequests.start()
|
||||||
|
.requestPluginTranslations("de")
|
||||||
|
.assertStatusCode(200)
|
||||||
|
.assertSingleProperty(value -> assertThat(value).isNotNull(), "scm-git-plugin")
|
||||||
|
.assertSingleProperty(value -> assertThat(value).isNotNull(), "scm-hg-plugin")
|
||||||
|
.assertSingleProperty(value -> assertThat(value).isNotNull(), "scm-svn-plugin");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -200,7 +200,7 @@ public class RepositoryAccessITCase {
|
|||||||
.then()
|
.then()
|
||||||
.statusCode(HttpStatus.SC_OK)
|
.statusCode(HttpStatus.SC_OK)
|
||||||
.extract()
|
.extract()
|
||||||
.path("_embedded.files.find{it.name=='a.txt'}._links.self.href");
|
.path("_embedded.children.find{it.name=='a.txt'}._links.self.href");
|
||||||
|
|
||||||
given()
|
given()
|
||||||
.when()
|
.when()
|
||||||
@@ -215,7 +215,7 @@ public class RepositoryAccessITCase {
|
|||||||
.then()
|
.then()
|
||||||
.statusCode(HttpStatus.SC_OK)
|
.statusCode(HttpStatus.SC_OK)
|
||||||
.extract()
|
.extract()
|
||||||
.path("_embedded.files.find{it.name=='subfolder'}._links.self.href");
|
.path("_embedded.children.find{it.name=='subfolder'}._links.self.href");
|
||||||
String selfOfSubfolderUrl = given()
|
String selfOfSubfolderUrl = given()
|
||||||
.when()
|
.when()
|
||||||
.get(subfolderSourceUrl)
|
.get(subfolderSourceUrl)
|
||||||
@@ -230,7 +230,7 @@ public class RepositoryAccessITCase {
|
|||||||
.then()
|
.then()
|
||||||
.statusCode(HttpStatus.SC_OK)
|
.statusCode(HttpStatus.SC_OK)
|
||||||
.extract()
|
.extract()
|
||||||
.path("_embedded.files[0]._links.self.href");
|
.path("_embedded.children[0]._links.self.href");
|
||||||
given()
|
given()
|
||||||
.when()
|
.when()
|
||||||
.get(subfolderContentUrl)
|
.get(subfolderContentUrl)
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package sonia.scm.it.utils;
|
|||||||
|
|
||||||
import io.restassured.RestAssured;
|
import io.restassured.RestAssured;
|
||||||
import io.restassured.response.Response;
|
import io.restassured.response.Response;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import java.net.ConnectException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -52,7 +54,12 @@ public class ScmRequests {
|
|||||||
setUsername(username);
|
setUsername(username);
|
||||||
setPassword(password);
|
setPassword(password);
|
||||||
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password,newPassword)), null);
|
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password,newPassword)), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ModelResponse requestPluginTranslations(String language) {
|
||||||
|
Response response = applyGETRequest(RestUtil.BASE_URL.resolve("locales/" + language + "/plugins.json").toString());
|
||||||
|
return new ModelResponse(response, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,10 +82,12 @@ public class ScmRequests {
|
|||||||
* @return the response of the GET request using the given link
|
* @return the response of the GET request using the given link
|
||||||
*/
|
*/
|
||||||
private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) {
|
private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) {
|
||||||
return applyGETRequestWithQueryParams(response
|
String url = response
|
||||||
.then()
|
.then()
|
||||||
.extract()
|
.extract()
|
||||||
.path(linkPropertyName), params);
|
.path(linkPropertyName);
|
||||||
|
Assert.assertNotNull("no url found for link " + linkPropertyName, url);
|
||||||
|
return applyGETRequestWithQueryParams(url, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,6 +99,11 @@ public class ScmRequests {
|
|||||||
*/
|
*/
|
||||||
private Response applyGETRequestWithQueryParams(String url, String params) {
|
private Response applyGETRequestWithQueryParams(String url, String params) {
|
||||||
LOG.info("GET {}", url);
|
LOG.info("GET {}", url);
|
||||||
|
if (username == null || password == null){
|
||||||
|
return RestAssured.given()
|
||||||
|
.when()
|
||||||
|
.get(url + params);
|
||||||
|
}
|
||||||
return RestAssured.given()
|
return RestAssured.given()
|
||||||
.auth().preemptive().basic(username, password)
|
.auth().preemptive().basic(username, password)
|
||||||
.when()
|
.when()
|
||||||
@@ -249,11 +263,11 @@ public class ScmRequests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ChangesetsResponse<SourcesResponse> requestFileHistory(String fileName) {
|
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) {
|
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 --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import org.eclipse.jgit.errors.MissingObjectException;
|
|
||||||
import org.eclipse.jgit.lib.Constants;
|
import org.eclipse.jgit.lib.Constants;
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.lib.ObjectLoader;
|
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.eclipse.jgit.treewalk.filter.TreeFilter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.GitSubModuleParser;
|
import sonia.scm.repository.GitSubModuleParser;
|
||||||
@@ -103,10 +104,11 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public BrowserResult getBrowserResult(BrowseCommandRequest request)
|
public BrowserResult getBrowserResult(BrowseCommandRequest request)
|
||||||
throws IOException, RevisionNotFoundException {
|
throws IOException, NotFoundException {
|
||||||
logger.debug("try to create browse result for {}", request);
|
logger.debug("try to create browse result for {}", request);
|
||||||
|
|
||||||
BrowserResult result;
|
BrowserResult result;
|
||||||
|
|
||||||
org.eclipse.jgit.lib.Repository repo = open();
|
org.eclipse.jgit.lib.Repository repo = open();
|
||||||
ObjectId revId;
|
ObjectId revId;
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
|
|
||||||
if (revId != null)
|
if (revId != null)
|
||||||
{
|
{
|
||||||
result = getResult(repo, request, revId);
|
result = new BrowserResult(revId.getName(), getEntry(repo, request, revId));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -134,8 +136,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
logger.warn("coul not find head of repository, empty?");
|
logger.warn("coul not find head of repository, empty?");
|
||||||
}
|
}
|
||||||
|
|
||||||
result = new BrowserResult(Constants.HEAD, null, null,
|
result = new BrowserResult(Constants.HEAD, createEmtpyRoot());
|
||||||
Collections.EMPTY_LIST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -143,6 +144,14 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
|
private FileObject createEmtpyRoot() {
|
||||||
|
FileObject fileObject = new FileObject();
|
||||||
|
fileObject.setName("");
|
||||||
|
fileObject.setPath("");
|
||||||
|
fileObject.setDirectory(true);
|
||||||
|
return fileObject;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
@@ -158,11 +167,8 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
|
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
|
||||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
|
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
|
||||||
throws IOException, RevisionNotFoundException {
|
throws IOException, RevisionNotFoundException {
|
||||||
FileObject file;
|
|
||||||
|
|
||||||
try
|
FileObject file = new FileObject();
|
||||||
{
|
|
||||||
file = new FileObject();
|
|
||||||
|
|
||||||
String path = treeWalk.getPathString();
|
String path = treeWalk.getPathString();
|
||||||
|
|
||||||
@@ -193,7 +199,6 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
if (!file.isDirectory() &&!request.isDisableLastCommit())
|
if (!file.isDirectory() &&!request.isDisableLastCommit())
|
||||||
{
|
{
|
||||||
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
||||||
|
|
||||||
RevCommit commit = getLatestCommit(repo, revId, path);
|
RevCommit commit = getLatestCommit(repo, revId, path);
|
||||||
|
|
||||||
if (commit != null)
|
if (commit != null)
|
||||||
@@ -208,18 +213,6 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,22 +258,19 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BrowserResult getResult(org.eclipse.jgit.lib.Repository repo,
|
private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException, NotFoundException {
|
||||||
BrowseCommandRequest request, ObjectId revId)
|
|
||||||
throws IOException, RevisionNotFoundException {
|
|
||||||
BrowserResult result = null;
|
|
||||||
RevWalk revWalk = null;
|
RevWalk revWalk = null;
|
||||||
TreeWalk treeWalk = null;
|
TreeWalk treeWalk = null;
|
||||||
|
|
||||||
try
|
FileObject result;
|
||||||
{
|
|
||||||
if (logger.isDebugEnabled())
|
try {
|
||||||
{
|
|
||||||
logger.debug("load repository browser for revision {}", revId.name());
|
logger.debug("load repository browser for revision {}", revId.name());
|
||||||
}
|
|
||||||
|
|
||||||
treeWalk = new TreeWalk(repo);
|
treeWalk = new TreeWalk(repo);
|
||||||
treeWalk.setRecursive(request.isRecursive());
|
if (!isRootRequest(request)) {
|
||||||
|
treeWalk.setFilter(PathFilter.create(request.getPath()));
|
||||||
|
}
|
||||||
revWalk = new RevWalk(repo);
|
revWalk = new RevWalk(repo);
|
||||||
|
|
||||||
RevTree tree = revWalk.parseTree(revId);
|
RevTree tree = revWalk.parseTree(revId);
|
||||||
@@ -291,65 +281,20 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.error("could not find tree for {}", revId.name());
|
throw new IllegalStateException("could not find tree for " + revId.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
result = new BrowserResult();
|
if (isRootRequest(request)) {
|
||||||
|
result = createEmtpyRoot();
|
||||||
List<FileObject> files = Lists.newArrayList();
|
findChildren(result, repo, request, revId, treeWalk);
|
||||||
|
} else {
|
||||||
String path = request.getPath();
|
result = findFirstMatch(repo, request, revId, treeWalk);
|
||||||
|
if ( result.isDirectory() ) {
|
||||||
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();
|
treeWalk.enterSubtree();
|
||||||
}
|
findChildren(result, repo, request, revId, treeWalk);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.setFiles(files);
|
|
||||||
result.setRevision(revId.getName());
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -360,6 +305,60 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
return result;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private Map<String,
|
private Map<String,
|
||||||
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,
|
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { repositories } from "@scm-manager/ui-components";
|
import { repositories } from "@scm-manager/ui-components";
|
||||||
import type { Repository } from "@scm-manager/ui-types";
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository
|
repository: Repository,
|
||||||
|
t: string => string
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProtocolInformation extends React.Component<Props> {
|
class ProtocolInformation extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repository } = this.props;
|
const { repository, t } = this.props;
|
||||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||||
if (!href) {
|
if (!href) {
|
||||||
return null;
|
return null;
|
||||||
@@ -18,11 +20,11 @@ class ProtocolInformation extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4>Clone the repository</h4>
|
<h4>{t("scm-git-plugin.information.clone")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>git clone {href}</code>
|
<code>git clone {href}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>Create a new repository</h4>
|
<h4>{t("scm-git-plugin.information.create")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>
|
<code>
|
||||||
git init {repository.name}
|
git init {repository.name}
|
||||||
@@ -39,7 +41,7 @@ class ProtocolInformation extends React.Component<Props> {
|
|||||||
<br />
|
<br />
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>Push an existing repository</h4>
|
<h4>{t("scm-git-plugin.information.replace")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>
|
<code>
|
||||||
git remote add origin {href}
|
git remote add origin {href}
|
||||||
@@ -54,4 +56,4 @@ class ProtocolInformation extends React.Component<Props> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProtocolInformation;
|
export default translate("plugins")(ProtocolInformation);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"scm-git-plugin": {
|
||||||
|
"information": {
|
||||||
|
"clone" : "Repository Klonen",
|
||||||
|
"create" : "Neue Repository erstellen",
|
||||||
|
"replace" : "Eine existierende Repository aktualisieren"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"scm-git-plugin": {
|
||||||
|
"information": {
|
||||||
|
"clone" : "Clone the repository",
|
||||||
|
"create" : "Create a new repository",
|
||||||
|
"replace" : "Push an existing repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,152 +26,114 @@
|
|||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
*
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
* http://bitbucket.org/sdorra/scm-manager
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.GitConstants;
|
import sonia.scm.repository.GitConstants;
|
||||||
import sonia.scm.repository.RevisionNotFoundException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link GitBrowseCommand}.
|
* Unit tests for {@link GitBrowseCommand}.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
public class GitBrowseCommandTest extends AbstractGitCommandTestBase
|
public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test browse command with default branch.
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testDefaultBranch() throws IOException, RevisionNotFoundException {
|
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 testDefaultDefaultBranch() throws IOException, NotFoundException {
|
||||||
// without default branch, the repository head should be used
|
// without default branch, the repository head should be used
|
||||||
BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest());
|
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
|
||||||
assertNotNull(result);
|
assertNotNull(root);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
Collection<FileObject> foList = root.getChildren();
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
assertFalse(foList.isEmpty());
|
||||||
assertEquals(4, foList.size());
|
|
||||||
|
|
||||||
assertEquals("a.txt", foList.get(0).getName());
|
assertThat(foList)
|
||||||
assertEquals("b.txt", foList.get(1).getName());
|
.extracting("name")
|
||||||
assertEquals("c", foList.get(2).getName());
|
.containsExactly("a.txt", "b.txt", "c", "f.txt");
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrowse() throws IOException, RevisionNotFoundException {
|
public void testExplicitDefaultBranch() throws IOException, NotFoundException {
|
||||||
BrowserResult result =
|
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
||||||
createCommand().getBrowserResult(new BrowseCommandRequest());
|
|
||||||
|
|
||||||
assertNotNull(result);
|
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
|
||||||
|
assertNotNull(root);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
Collection<FileObject> foList = root.getChildren();
|
||||||
|
assertThat(foList)
|
||||||
assertNotNull(foList);
|
.extracting("name")
|
||||||
assertFalse(foList.isEmpty());
|
.containsExactly("a.txt", "c");
|
||||||
assertEquals(4, foList.size());
|
|
||||||
|
|
||||||
FileObject a = null;
|
|
||||||
FileObject c = null;
|
|
||||||
|
|
||||||
for (FileObject f : foList)
|
|
||||||
{
|
|
||||||
if ("a.txt".equals(f.getName()))
|
|
||||||
{
|
|
||||||
a = f;
|
|
||||||
}
|
|
||||||
else if ("c".equals(f.getName()))
|
|
||||||
{
|
|
||||||
c = f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotNull(a);
|
@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");
|
||||||
|
|
||||||
assertFalse(a.isDirectory());
|
assertFalse(a.isDirectory());
|
||||||
assertEquals("a.txt", a.getName());
|
assertEquals("a.txt", a.getName());
|
||||||
assertEquals("a.txt", a.getPath());
|
assertEquals("a.txt", a.getPath());
|
||||||
assertEquals("added new line for blame", a.getDescription());
|
assertEquals("added new line for blame", a.getDescription());
|
||||||
assertTrue(a.getLength() > 0);
|
assertTrue(a.getLength() > 0);
|
||||||
checkDate(a.getLastModified());
|
checkDate(a.getLastModified());
|
||||||
assertNotNull(c);
|
|
||||||
assertTrue(c.isDirectory());
|
assertTrue(c.isDirectory());
|
||||||
assertEquals("c", c.getName());
|
assertEquals("c", c.getName());
|
||||||
assertEquals("c", c.getPath());
|
assertEquals("c", c.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrowseSubDirectory() throws IOException, RevisionNotFoundException {
|
public void testBrowseSubDirectory() throws IOException, NotFoundException {
|
||||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||||
|
|
||||||
request.setPath("c");
|
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);
|
FileObject d = findFile(foList, "d.txt");
|
||||||
assertFalse(foList.isEmpty());
|
FileObject e = findFile(foList, "e.txt");
|
||||||
assertEquals(2, foList.size());
|
|
||||||
|
|
||||||
FileObject d = null;
|
|
||||||
FileObject e = null;
|
|
||||||
|
|
||||||
for (FileObject f : foList)
|
|
||||||
{
|
|
||||||
if ("d.txt".equals(f.getName()))
|
|
||||||
{
|
|
||||||
d = f;
|
|
||||||
}
|
|
||||||
else if ("e.txt".equals(f.getName()))
|
|
||||||
{
|
|
||||||
e = f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotNull(d);
|
|
||||||
assertFalse(d.isDirectory());
|
assertFalse(d.isDirectory());
|
||||||
assertEquals("d.txt", d.getName());
|
assertEquals("d.txt", d.getName());
|
||||||
assertEquals("c/d.txt", d.getPath());
|
assertEquals("c/d.txt", d.getPath());
|
||||||
assertEquals("added file d and e in folder c", d.getDescription());
|
assertEquals("added file d and e in folder c", d.getDescription());
|
||||||
assertTrue(d.getLength() > 0);
|
assertTrue(d.getLength() > 0);
|
||||||
checkDate(d.getLastModified());
|
checkDate(d.getLastModified());
|
||||||
assertNotNull(e);
|
|
||||||
assertFalse(e.isDirectory());
|
assertFalse(e.isDirectory());
|
||||||
assertEquals("e.txt", e.getName());
|
assertEquals("e.txt", e.getName());
|
||||||
assertEquals("c/e.txt", e.getPath());
|
assertEquals("c/e.txt", e.getPath());
|
||||||
@@ -181,30 +143,35 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRecusive() throws IOException, RevisionNotFoundException {
|
public void testRecursive() throws IOException, NotFoundException {
|
||||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||||
|
|
||||||
request.setRecursive(true);
|
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);
|
FileObject c = findFile(foList, "c");
|
||||||
assertFalse(foList.isEmpty());
|
|
||||||
assertEquals(5, foList.size());
|
Collection<FileObject> cChildren = c.getChildren();
|
||||||
|
assertThat(cChildren)
|
||||||
|
.extracting("name")
|
||||||
|
.containsExactly("d.txt", "e.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private FileObject findFile(Collection<FileObject> foList, String name) {
|
||||||
* Method description
|
return foList.stream()
|
||||||
*
|
.filter(f -> name.equals(f.getName()))
|
||||||
*
|
.findFirst()
|
||||||
* @return
|
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||||
*/
|
}
|
||||||
private GitBrowseCommand createCommand()
|
|
||||||
{
|
private GitBrowseCommand createCommand() {
|
||||||
return new GitBrowseCommand(createContext(), repository);
|
return new GitBrowseCommand(createContext(), repository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.spi.javahg.HgFileviewCommand;
|
import sonia.scm.repository.spi.javahg.HgFileviewCommand;
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ import java.io.IOException;
|
|||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Utilizes the mercurial fileview extension in order to support mercurial repository browsing.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@@ -94,16 +97,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
|
|||||||
cmd.disableSubRepositoryDetection();
|
cmd.disableSubRepositoryDetection();
|
||||||
}
|
}
|
||||||
|
|
||||||
BrowserResult result = new BrowserResult();
|
FileObject file = cmd.execute();
|
||||||
|
return new BrowserResult(MoreObjects.firstNonNull(request.getRevision(), "tip"), file);
|
||||||
result.setFiles(cmd.execute());
|
|
||||||
|
|
||||||
if (!Strings.isNullOrEmpty(request.getRevision())) {
|
|
||||||
result.setRevision(request.getRevision());
|
|
||||||
} else {
|
|
||||||
result.setRevision("tip");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,35 +50,31 @@ import sonia.scm.repository.SubRepository;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Mercurial command to list files of a repository.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
public class HgFileviewCommand extends AbstractCommand
|
public class HgFileviewCommand extends AbstractCommand
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
private boolean disableLastCommit = false;
|
||||||
* Constructs ...
|
|
||||||
*
|
private HgFileviewCommand(Repository repository)
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
*/
|
|
||||||
public HgFileviewCommand(Repository repository)
|
|
||||||
{
|
{
|
||||||
super(repository);
|
super(repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Create command for the given repository.
|
||||||
*
|
*
|
||||||
|
* @param repository repository
|
||||||
*
|
*
|
||||||
* @param repository
|
* @return fileview command
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static HgFileviewCommand on(Repository repository)
|
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 {@code this}
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public HgFileviewCommand disableLastCommit()
|
public HgFileviewCommand disableLastCommit() {
|
||||||
{
|
|
||||||
disableLastCommit = true;
|
disableLastCommit = true;
|
||||||
cmdAppend("-d");
|
cmdAppend("-d");
|
||||||
|
|
||||||
@@ -100,132 +94,128 @@ public class HgFileviewCommand extends AbstractCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Disables sub repository detection
|
||||||
*
|
*
|
||||||
*
|
* @return {@code this}
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public HgFileviewCommand disableSubRepositoryDetection()
|
public HgFileviewCommand disableSubRepositoryDetection() {
|
||||||
{
|
|
||||||
cmdAppend("-s");
|
cmdAppend("-s");
|
||||||
|
|
||||||
return this;
|
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
|
public HgFileviewCommand path(String path) {
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
cmdAppend("-p", path);
|
cmdAppend("-p", path);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Fetch file objects recursive.
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @return
|
* @return {@code this}
|
||||||
*/
|
*/
|
||||||
public HgFileviewCommand recursive()
|
public HgFileviewCommand recursive() {
|
||||||
{
|
|
||||||
cmdAppend("-c");
|
cmdAppend("-c");
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Use given revision for file view.
|
||||||
*
|
*
|
||||||
|
* @param revision revision id, hash, tag or branch
|
||||||
*
|
*
|
||||||
* @param revision
|
* @return {@code this}
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public HgFileviewCommand rev(String revision)
|
public HgFileviewCommand rev(String revision) {
|
||||||
{
|
|
||||||
cmdAppend("-r", revision);
|
cmdAppend("-r", revision);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Executes the mercurial command and parses the output.
|
||||||
*
|
*
|
||||||
*
|
* @return file object
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getCommandName()
|
|
||||||
{
|
|
||||||
return HgFileviewExtension.NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param stream
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*
|
*
|
||||||
* @throws IOException
|
* @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();
|
FileObject directory = new FileObject();
|
||||||
String path = removeTrailingSlash(stream.textUpTo('\0'));
|
String path = removeTrailingSlash(stream.textUpTo('\0'));
|
||||||
|
|
||||||
@@ -236,18 +226,7 @@ public class HgFileviewCommand extends AbstractCommand
|
|||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private FileObject readFile(HgInputStream stream) throws IOException {
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param stream
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private FileObject readFile(HgInputStream stream) throws IOException
|
|
||||||
{
|
|
||||||
FileObject file = new FileObject();
|
FileObject file = new FileObject();
|
||||||
String path = removeTrailingSlash(stream.textUpTo('\n'));
|
String path = removeTrailingSlash(stream.textUpTo('\n'));
|
||||||
|
|
||||||
@@ -259,8 +238,7 @@ public class HgFileviewCommand extends AbstractCommand
|
|||||||
DateTime timestamp = stream.dateTimeUpTo(' ');
|
DateTime timestamp = stream.dateTimeUpTo(' ');
|
||||||
String description = stream.textUpTo('\0');
|
String description = stream.textUpTo('\0');
|
||||||
|
|
||||||
if (!disableLastCommit)
|
if (!disableLastCommit) {
|
||||||
{
|
|
||||||
file.setLastModified(timestamp.getDate().getTime());
|
file.setLastModified(timestamp.getDate().getTime());
|
||||||
file.setDescription(description);
|
file.setDescription(description);
|
||||||
}
|
}
|
||||||
@@ -268,18 +246,7 @@ public class HgFileviewCommand extends AbstractCommand
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private FileObject readSubRepository(HgInputStream stream) throws IOException {
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param stream
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private FileObject readSubRepository(HgInputStream stream) throws IOException
|
|
||||||
{
|
|
||||||
FileObject directory = new FileObject();
|
FileObject directory = new FileObject();
|
||||||
String path = removeTrailingSlash(stream.textUpTo('\n'));
|
String path = removeTrailingSlash(stream.textUpTo('\n'));
|
||||||
|
|
||||||
@@ -292,8 +259,7 @@ public class HgFileviewCommand extends AbstractCommand
|
|||||||
|
|
||||||
SubRepository subRepository = new SubRepository(url);
|
SubRepository subRepository = new SubRepository(url);
|
||||||
|
|
||||||
if (!Strings.isNullOrEmpty(revision))
|
if (!Strings.isNullOrEmpty(revision)) {
|
||||||
{
|
|
||||||
subRepository.setRevision(revision);
|
subRepository.setRevision(revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,48 +268,33 @@ public class HgFileviewCommand extends AbstractCommand
|
|||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String removeTrailingSlash(String path) {
|
||||||
* Method description
|
if (path.endsWith("/")) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String removeTrailingSlash(String path)
|
|
||||||
{
|
|
||||||
if (path.endsWith("/"))
|
|
||||||
{
|
|
||||||
path = path.substring(0, path.length() - 1);
|
path = path.substring(0, path.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
private String getNameFromPath(String path) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String getNameFromPath(String path)
|
|
||||||
{
|
|
||||||
int index = path.lastIndexOf('/');
|
int index = path.lastIndexOf('/');
|
||||||
|
|
||||||
if (index > 0)
|
if (index > 0) {
|
||||||
{
|
|
||||||
path = path.substring(index + 1);
|
path = path.substring(index + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,28 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { repositories } from "@scm-manager/ui-components";
|
import { repositories } from "@scm-manager/ui-components";
|
||||||
import type { Repository } from "@scm-manager/ui-types";
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository
|
repository: Repository,
|
||||||
|
t: string => string
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProtocolInformation extends React.Component<Props> {
|
class ProtocolInformation extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repository } = this.props;
|
const { repository, t } = this.props;
|
||||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||||
if (!href) {
|
if (!href) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4>Clone the repository</h4>
|
<h4>{t("scm-hg-plugin.information.clone")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>hg clone {href}</code>
|
<code>hg clone {href}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>Create a new repository</h4>
|
<h4>{t("scm-hg-plugin.information.create")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>
|
<code>
|
||||||
hg init {repository.name}
|
hg init {repository.name}
|
||||||
@@ -41,7 +43,7 @@ class ProtocolInformation extends React.Component<Props> {
|
|||||||
<br />
|
<br />
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>Push an existing repository</h4>
|
<h4>{t("scm-hg-plugin.information.replace")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>
|
<code>
|
||||||
# add the repository url as default to your .hg/hgrc e.g:
|
# add the repository url as default to your .hg/hgrc e.g:
|
||||||
@@ -59,4 +61,4 @@ class ProtocolInformation extends React.Component<Props> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProtocolInformation;
|
export default translate("plugins")(ProtocolInformation);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"scm-hg-plugin": {
|
||||||
|
"information": {
|
||||||
|
"clone" : "Repository Klonen",
|
||||||
|
"create" : "Neue Repository erstellen",
|
||||||
|
"replace" : "Eine existierende Repository aktualisieren"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"scm-hg-plugin": {
|
||||||
|
"information": {
|
||||||
|
"clone" : "Clone the repository",
|
||||||
|
"create" : "Create a new repository",
|
||||||
|
"replace" : "Push an existing repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,61 +32,129 @@
|
|||||||
|
|
||||||
Prints date, size and last message of files.
|
Prints date, size and last message of files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
from mercurial import cmdutil,util
|
from mercurial import cmdutil,util
|
||||||
|
|
||||||
cmdtable = {}
|
cmdtable = {}
|
||||||
command = cmdutil.command(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:
|
class SubRepository:
|
||||||
url = None
|
url = None
|
||||||
revision = None
|
revision = None
|
||||||
|
|
||||||
def removeTrailingSlash(path):
|
def collect_sub_repositories(revCtx):
|
||||||
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):
|
|
||||||
subrepos = {}
|
subrepos = {}
|
||||||
try:
|
try:
|
||||||
hgsub = revCtx.filectx('.hgsub').data().split('\n')
|
hgsub = revCtx.filectx('.hgsub').data().split('\n')
|
||||||
@@ -112,29 +180,74 @@ def createSubRepositoryMap(revCtx):
|
|||||||
|
|
||||||
return subrepos
|
return subrepos
|
||||||
|
|
||||||
def printSubRepository(ui, path, subrepository, transport):
|
class File_Printer:
|
||||||
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):
|
def __init__(self, ui, repo, revCtx, disableLastCommit, transport):
|
||||||
format = '%s\n'
|
self.ui = ui
|
||||||
if transport:
|
self.repo = repo
|
||||||
format = 'd%s\0'
|
self.revCtx = revCtx
|
||||||
ui.write( format % path)
|
self.disableLastCommit = disableLastCommit
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
def printFile(ui, repo, file, disableLastCommit, 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'
|
date = '0 0'
|
||||||
description = 'n/a'
|
description = 'n/a'
|
||||||
if not disableLastCommit:
|
if not self.disableLastCommit:
|
||||||
linkrev = repo[file.linkrev()]
|
linkrev = self.repo[file.linkrev()]
|
||||||
date = '%d %d' % util.parsedate(linkrev.date())
|
date = '%d %d' % util.parsedate(linkrev.date())
|
||||||
description = linkrev.description()
|
description = linkrev.description()
|
||||||
format = '%s %i %s %s\n'
|
format = '%s %i %s %s\n'
|
||||||
if transport:
|
if self.transport:
|
||||||
format = 'f%s\n%i %s %s\0'
|
format = 'f%s\n%i %s %s\0'
|
||||||
ui.write( format % (file.path(), file.size(), date, description) )
|
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', [
|
@command('fileview', [
|
||||||
('r', 'revision', 'tip', 'revision to print'),
|
('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'),
|
('t', 'transport', False, 'format the output for command server'),
|
||||||
])
|
])
|
||||||
def fileview(ui, repo, **opts):
|
def fileview(ui, repo, **opts):
|
||||||
files = []
|
revCtx = repo[opts["revision"]]
|
||||||
directories = []
|
subrepos = {}
|
||||||
revision = opts['revision']
|
if not opts["disableSubRepositoryDetection"]:
|
||||||
if revision == None:
|
subrepos = collect_sub_repositories(revCtx)
|
||||||
revision = 'tip'
|
printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"])
|
||||||
revCtx = repo[revision]
|
viewer = File_Viewer(revCtx, printer)
|
||||||
path = opts['path']
|
viewer.recursive = opts["recursive"]
|
||||||
if path.endswith('/'):
|
viewer.sub_repositories = subrepos
|
||||||
path = path[0:-1]
|
viewer.view(opts["path"])
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.Collection;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
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.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @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
|
@Test
|
||||||
public void testBrowse() throws IOException {
|
public void testBrowse() throws IOException {
|
||||||
List<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||||
FileObject a = getFileObject(foList, "a.txt");
|
FileObject a = getFileObject(foList, "a.txt");
|
||||||
FileObject c = getFileObject(foList, "c");
|
FileObject c = getFileObject(foList, "c");
|
||||||
|
|
||||||
@@ -85,7 +90,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
|||||||
|
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
FileObject c = result.getFile();
|
||||||
|
assertEquals("c", c.getName());
|
||||||
|
Collection<FileObject> foList = c.getChildren();
|
||||||
|
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
assertFalse(foList.isEmpty());
|
||||||
@@ -128,7 +135,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
|||||||
|
|
||||||
request.setDisableLastCommit(true);
|
request.setDisableLastCommit(true);
|
||||||
|
|
||||||
List<FileObject> foList = getRootFromTip(request);
|
Collection<FileObject> foList = getRootFromTip(request);
|
||||||
|
|
||||||
FileObject a = getFileObject(foList, "a.txt");
|
FileObject a = getFileObject(foList, "a.txt");
|
||||||
|
|
||||||
@@ -147,11 +154,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
|||||||
|
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
FileObject root = result.getFile();
|
||||||
|
Collection<FileObject> foList = root.getChildren();
|
||||||
|
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
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 ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
@@ -165,32 +177,22 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
|||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private FileObject getFileObject(List<FileObject> foList, String name)
|
private FileObject getFileObject(Collection<FileObject> foList, String name)
|
||||||
{
|
{
|
||||||
FileObject a = null;
|
return foList.stream()
|
||||||
|
.filter(f -> name.equals(f.getName()))
|
||||||
for (FileObject f : foList)
|
.findFirst()
|
||||||
{
|
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||||
if (name.equals(f.getName()))
|
|
||||||
{
|
|
||||||
a = f;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotNull(a);
|
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
|
|
||||||
BrowserResult result = new HgBrowseCommand(cmdContext,
|
BrowserResult result = new HgBrowseCommand(cmdContext,
|
||||||
repository).getBrowserResult(request);
|
repository).getBrowserResult(request);
|
||||||
|
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
FileObject root = result.getFile();
|
||||||
|
Collection<FileObject> foList = root.getChildren();
|
||||||
|
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
assertFalse(foList.isEmpty());
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -79,11 +80,11 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
|||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException {
|
public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||||
String path = request.getPath();
|
String path = Strings.nullToEmpty(request.getPath());
|
||||||
long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision());
|
long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision());
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
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;
|
BrowserResult result = null;
|
||||||
@@ -91,34 +92,21 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
SVNRepository svnRepository = open();
|
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) {
|
if (revisionNumber == -1) {
|
||||||
revisionNumber = svnRepository.getLatestRevision();
|
revisionNumber = svnRepository.getLatestRevision();
|
||||||
}
|
}
|
||||||
|
|
||||||
result = new BrowserResult();
|
SVNDirEntry rootEntry = svnRepository.info(path, revisionNumber);
|
||||||
result.setRevision(String.valueOf(revisionNumber));
|
FileObject root = createFileObject(request, svnRepository, revisionNumber, rootEntry, path);
|
||||||
result.setFiles(children);
|
root.setPath(path);
|
||||||
|
|
||||||
|
if (root.isDirectory()) {
|
||||||
|
traverse(svnRepository, revisionNumber, request, root, createBasePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
result = new BrowserResult(String.valueOf(revisionNumber), root);
|
||||||
}
|
}
|
||||||
catch (SVNException ex)
|
catch (SVNException ex)
|
||||||
{
|
{
|
||||||
@@ -130,52 +118,24 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
|||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param svnRepository
|
|
||||||
* @param revisionNumber
|
|
||||||
* @param request
|
|
||||||
* @param children
|
|
||||||
* @param entries
|
|
||||||
* @param basePath
|
|
||||||
*
|
|
||||||
* @throws SVNException
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void browseRecursive(SVNRepository svnRepository,
|
private void traverse(SVNRepository svnRepository, long revisionNumber, BrowseCommandRequest request,
|
||||||
long revisionNumber, BrowseCommandRequest request,
|
FileObject parent, String basePath)
|
||||||
List<FileObject> children, Collection<SVNDirEntry> entries, String basePath)
|
|
||||||
throws SVNException
|
throws SVNException
|
||||||
{
|
{
|
||||||
|
Collection<SVNDirEntry> entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null);
|
||||||
for (SVNDirEntry entry : entries)
|
for (SVNDirEntry entry : entries)
|
||||||
{
|
{
|
||||||
FileObject fo = createFileObject(request, svnRepository, revisionNumber,
|
FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath);
|
||||||
entry, basePath);
|
|
||||||
|
|
||||||
children.add(fo);
|
parent.addChild(child);
|
||||||
|
|
||||||
if (fo.isDirectory())
|
if (child.isDirectory() && request.isRecursive()) {
|
||||||
{
|
traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath()));
|
||||||
Collection<SVNDirEntry> subEntries =
|
|
||||||
svnRepository.getDir(Util.nonNull(fo.getPath()), revisionNumber,
|
|
||||||
null, (Collection) null);
|
|
||||||
|
|
||||||
browseRecursive(svnRepository, revisionNumber, request, children,
|
|
||||||
subEntries, createBasePath(fo.getPath()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String createBasePath(String path)
|
private String createBasePath(String path)
|
||||||
{
|
{
|
||||||
String basePath = Util.EMPTY_STRING;
|
String basePath = Util.EMPTY_STRING;
|
||||||
@@ -193,20 +153,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
|||||||
return basePath;
|
return basePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param repository
|
|
||||||
* @param revision
|
|
||||||
* @param entry
|
|
||||||
* @param path
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private FileObject createFileObject(BrowseCommandRequest request,
|
private FileObject createFileObject(BrowseCommandRequest request,
|
||||||
SVNRepository repository, long revision, SVNDirEntry entry, String path)
|
SVNRepository repository, long revision, SVNDirEntry entry, String path)
|
||||||
{
|
{
|
||||||
@@ -237,15 +183,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
|||||||
return fileObject;
|
return fileObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
* @param revision
|
|
||||||
* @param entry
|
|
||||||
* @param fileObject
|
|
||||||
*/
|
|
||||||
private void fetchExternalsProperty(SVNRepository repository, long revision,
|
private void fetchExternalsProperty(SVNRepository repository, long revision,
|
||||||
SVNDirEntry entry, FileObject fileObject)
|
SVNDirEntry entry, FileObject fileObject)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,22 +2,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { repositories } from "@scm-manager/ui-components";
|
import { repositories } from "@scm-manager/ui-components";
|
||||||
import type { Repository } from "@scm-manager/ui-types";
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository
|
repository: Repository,
|
||||||
|
t: string => string
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProtocolInformation extends React.Component<Props> {
|
class ProtocolInformation extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repository } = this.props;
|
const { repository, t } = this.props;
|
||||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||||
if (!href) {
|
if (!href) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4>Checkout the repository</h4>
|
<h4>{t("scm-svn-plugin.information.checkout")}</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<code>svn checkout {href}</code>
|
<code>svn checkout {href}</code>
|
||||||
</pre>
|
</pre>
|
||||||
@@ -27,4 +29,4 @@ class ProtocolInformation extends React.Component<Props> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProtocolInformation;
|
export default translate("plugins")(ProtocolInformation);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"scm-svn-plugin": {
|
||||||
|
"information": {
|
||||||
|
"checkout" : "Repository auschecken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"scm-svn-plugin": {
|
||||||
|
"information": {
|
||||||
|
"checkout" : "Checkout repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,15 +33,13 @@
|
|||||||
|
|
||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.RevisionNotFoundException;
|
import sonia.scm.repository.RevisionNotFoundException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.Collection;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
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.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
@@ -58,9 +54,19 @@ import static org.junit.Assert.assertTrue;
|
|||||||
public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
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
|
@Test
|
||||||
public void testBrowse() throws RevisionNotFoundException {
|
public void testBrowse() throws RevisionNotFoundException {
|
||||||
List<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||||
|
|
||||||
FileObject a = getFileObject(foList, "a.txt");
|
FileObject a = getFileObject(foList, "a.txt");
|
||||||
FileObject c = getFileObject(foList, "c");
|
FileObject c = getFileObject(foList, "c");
|
||||||
@@ -92,7 +98,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
|||||||
|
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
Collection<FileObject> foList = result.getFile().getChildren();
|
||||||
|
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
assertFalse(foList.isEmpty());
|
||||||
@@ -135,7 +141,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
|||||||
|
|
||||||
request.setDisableLastCommit(true);
|
request.setDisableLastCommit(true);
|
||||||
|
|
||||||
List<FileObject> foList = getRootFromTip(request);
|
Collection<FileObject> foList = getRootFromTip(request);
|
||||||
|
|
||||||
FileObject a = getFileObject(foList, "a.txt");
|
FileObject a = getFileObject(foList, "a.txt");
|
||||||
|
|
||||||
@@ -151,15 +157,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
|||||||
|
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
Collection<FileObject> foList = result.getFile().getChildren();
|
||||||
|
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
assertFalse(foList.isEmpty());
|
||||||
assertEquals(4, foList.size());
|
assertEquals(2, foList.size());
|
||||||
|
|
||||||
for ( FileObject fo : foList ){
|
FileObject c = getFileObject(foList, "c");
|
||||||
System.out.println(fo);
|
assertEquals("c", c.getName());
|
||||||
}
|
assertTrue(c.isDirectory());
|
||||||
|
assertEquals(2, c.getChildren().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,31 +191,20 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
|||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private FileObject getFileObject(List<FileObject> foList, String name)
|
private FileObject getFileObject(Collection<FileObject> foList, String name)
|
||||||
{
|
{
|
||||||
FileObject a = null;
|
return foList.stream()
|
||||||
|
.filter(f -> name.equals(f.getName()))
|
||||||
for (FileObject f : foList)
|
.findFirst()
|
||||||
{
|
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||||
if (name.equals(f.getName()))
|
|
||||||
{
|
|
||||||
a = f;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotNull(a);
|
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
|
|
||||||
BrowserResult result = createCommand().getBrowserResult(request);
|
BrowserResult result = createCommand().getBrowserResult(request);
|
||||||
|
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
|
|
||||||
List<FileObject> foList = result.getFiles();
|
Collection<FileObject> foList = result.getFile().getChildren();
|
||||||
|
|
||||||
assertNotNull(foList);
|
assertNotNull(foList);
|
||||||
assertFalse(foList.isEmpty());
|
assertFalse(foList.isEmpty());
|
||||||
|
|||||||
@@ -4,38 +4,58 @@ import { translate } from "react-i18next";
|
|||||||
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
t: string => string
|
t: string => string,
|
||||||
|
repositoriesLink: string,
|
||||||
|
usersLink: string,
|
||||||
|
groupsLink: string,
|
||||||
|
configLink: string,
|
||||||
|
logoutLink: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class PrimaryNavigation extends React.Component<Props> {
|
class PrimaryNavigation extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { t, repositoriesLink, usersLink, groupsLink, configLink, logoutLink } = this.props;
|
||||||
return (
|
|
||||||
<nav className="tabs is-boxed">
|
const links = [
|
||||||
<ul>
|
repositoriesLink ? (
|
||||||
<PrimaryNavigationLink
|
<PrimaryNavigationLink
|
||||||
to="/repos"
|
to="/repos"
|
||||||
match="/(repo|repos)"
|
match="/(repo|repos)"
|
||||||
label={t("primary-navigation.repositories")}
|
label={t("primary-navigation.repositories")}
|
||||||
/>
|
key={"repositoriesLink"}
|
||||||
|
/>): null,
|
||||||
|
usersLink ? (
|
||||||
<PrimaryNavigationLink
|
<PrimaryNavigationLink
|
||||||
to="/users"
|
to="/users"
|
||||||
match="/(user|users)"
|
match="/(user|users)"
|
||||||
label={t("primary-navigation.users")}
|
label={t("primary-navigation.users")}
|
||||||
/>
|
key={"usersLink"}
|
||||||
|
/>) : null,
|
||||||
|
groupsLink ? (
|
||||||
<PrimaryNavigationLink
|
<PrimaryNavigationLink
|
||||||
to="/groups"
|
to="/groups"
|
||||||
match="/(group|groups)"
|
match="/(group|groups)"
|
||||||
label={t("primary-navigation.groups")}
|
label={t("primary-navigation.groups")}
|
||||||
/>
|
key={"groupsLink"}
|
||||||
|
/>) : null,
|
||||||
|
configLink ? (
|
||||||
<PrimaryNavigationLink
|
<PrimaryNavigationLink
|
||||||
to="/config"
|
to="/config"
|
||||||
label={t("primary-navigation.config")}
|
label={t("primary-navigation.config")}
|
||||||
/>
|
key={"configLink"}
|
||||||
|
/>) : null,
|
||||||
|
logoutLink ? (
|
||||||
<PrimaryNavigationLink
|
<PrimaryNavigationLink
|
||||||
to="/logout"
|
to="/logout"
|
||||||
label={t("primary-navigation.logout")}
|
label={t("primary-navigation.logout")}
|
||||||
/>
|
key={"logoutLink"}
|
||||||
|
/>) : null
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="tabs is-boxed">
|
||||||
|
<ul>
|
||||||
|
{links}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
//@flow
|
||||||
|
import type { Links } from "./hal";
|
||||||
|
|
||||||
|
export type IndexResources = {
|
||||||
|
version: string,
|
||||||
|
_links: Links
|
||||||
|
};
|
||||||
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[]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,4 +17,8 @@ export type { Tag } from "./Tags";
|
|||||||
|
|
||||||
export type { Config } from "./Config";
|
export type { Config } from "./Config";
|
||||||
|
|
||||||
|
export type { IndexResources } from "./IndexResources";
|
||||||
|
|
||||||
export type { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions";
|
export type { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions";
|
||||||
|
|
||||||
|
export type { SubRepository, File } from "./Sources";
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"navigation-label": "Navigation",
|
"navigation-label": "Navigation",
|
||||||
"history": "Commits",
|
"history": "Commits",
|
||||||
"information": "Information",
|
"information": "Information",
|
||||||
"permissions": "Permissions"
|
"permissions": "Permissions",
|
||||||
|
"sources": "Sources"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Create Repository",
|
"title": "Create Repository",
|
||||||
@@ -45,6 +46,14 @@
|
|||||||
"cancel": "No"
|
"cancel": "No"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sources": {
|
||||||
|
"file-tree": {
|
||||||
|
"name": "Name",
|
||||||
|
"length": "Length",
|
||||||
|
"lastModified": "Last modified",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
},
|
||||||
"changesets": {
|
"changesets": {
|
||||||
"error-title": "Error",
|
"error-title": "Error",
|
||||||
"error-subtitle": "Could not fetch changesets",
|
"error-subtitle": "Could not fetch changesets",
|
||||||
@@ -86,6 +95,11 @@
|
|||||||
"add-permission-heading": "Add new Permission",
|
"add-permission-heading": "Add new Permission",
|
||||||
"submit-button": "Submit",
|
"submit-button": "Submit",
|
||||||
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
|
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"groupPermissionHelpText": "States if a permission is a group permission.",
|
||||||
|
"nameHelpText": "Manage permissions for a specific user or group",
|
||||||
|
"typeHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
|
|||||||
@@ -14,18 +14,20 @@ import {
|
|||||||
modifyConfigReset
|
modifyConfigReset
|
||||||
} from "../modules/config";
|
} from "../modules/config";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import type { Config } from "@scm-manager/ui-types";
|
import type { Config, Link } from "@scm-manager/ui-types";
|
||||||
import ConfigForm from "../components/form/ConfigForm";
|
import ConfigForm from "../components/form/ConfigForm";
|
||||||
|
import { getConfigLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
config: Config,
|
config: Config,
|
||||||
configUpdatePermission: boolean,
|
configUpdatePermission: boolean,
|
||||||
|
configLink: string,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
modifyConfig: (config: Config, callback?: () => void) => void,
|
modifyConfig: (config: Config, callback?: () => void) => void,
|
||||||
fetchConfig: void => void,
|
fetchConfig: (link: string) => void,
|
||||||
configReset: void => void,
|
configReset: void => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
@@ -35,7 +37,7 @@ type Props = {
|
|||||||
class GlobalConfig extends React.Component<Props> {
|
class GlobalConfig extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.configReset();
|
this.props.configReset();
|
||||||
this.props.fetchConfig();
|
this.props.fetchConfig(this.props.configLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyConfig = (config: Config) => {
|
modifyConfig = (config: Config) => {
|
||||||
@@ -75,8 +77,8 @@ class GlobalConfig extends React.Component<Props> {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchConfig: () => {
|
fetchConfig: (link: string) => {
|
||||||
dispatch(fetchConfig());
|
dispatch(fetchConfig(link));
|
||||||
},
|
},
|
||||||
modifyConfig: (config: Config, callback?: () => void) => {
|
modifyConfig: (config: Config, callback?: () => void) => {
|
||||||
dispatch(modifyConfig(config, callback));
|
dispatch(modifyConfig(config, callback));
|
||||||
@@ -92,12 +94,14 @@ const mapStateToProps = state => {
|
|||||||
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
const configUpdatePermission = getConfigUpdatePermission(state);
|
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||||
|
const configLink = getConfigLink(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
config,
|
config,
|
||||||
configUpdatePermission
|
configUpdatePermission,
|
||||||
|
configLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,14 @@ export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`;
|
|||||||
export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`;
|
export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`;
|
||||||
export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
|
export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
|
||||||
|
|
||||||
const CONFIG_URL = "config";
|
|
||||||
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
|
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
|
||||||
|
|
||||||
//fetch config
|
//fetch config
|
||||||
export function fetchConfig() {
|
export function fetchConfig(link: string) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchConfigPending());
|
dispatch(fetchConfigPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(CONFIG_URL)
|
.get(link)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ import reducer, {
|
|||||||
getConfig,
|
getConfig,
|
||||||
getConfigUpdatePermission
|
getConfigUpdatePermission
|
||||||
} from "./config";
|
} from "./config";
|
||||||
|
import { getConfigLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
const CONFIG_URL = "/api/v2/config";
|
const CONFIG_URL = "/config";
|
||||||
|
const URL = "/api/v2" + CONFIG_URL;
|
||||||
|
|
||||||
const error = new Error("You have an error!");
|
const error = new Error("You have an error!");
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ describe("config fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should successfully fetch config", () => {
|
it("should successfully fetch config", () => {
|
||||||
fetchMock.getOnce(CONFIG_URL, response);
|
fetchMock.getOnce(URL, response);
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: FETCH_CONFIG_PENDING },
|
{ type: FETCH_CONFIG_PENDING },
|
||||||
@@ -113,20 +115,36 @@ describe("config fetch()", () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({
|
||||||
|
indexResources: {
|
||||||
|
links: {
|
||||||
|
config: {
|
||||||
|
href: CONFIG_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return store.dispatch(fetchConfig()).then(() => {
|
return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail getting config on HTTP 500", () => {
|
it("should fail getting config on HTTP 500", () => {
|
||||||
fetchMock.getOnce(CONFIG_URL, {
|
fetchMock.getOnce(URL, {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({
|
||||||
return store.dispatch(fetchConfig()).then(() => {
|
indexResources: {
|
||||||
|
links: {
|
||||||
|
config: {
|
||||||
|
href: CONFIG_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
|
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);
|
||||||
|
|||||||
@@ -19,16 +19,33 @@ import {
|
|||||||
Footer,
|
Footer,
|
||||||
Header
|
Header
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import type { Me } from "@scm-manager/ui-types";
|
import type { Me, Link } from "@scm-manager/ui-types";
|
||||||
|
import {
|
||||||
|
fetchIndexResources,
|
||||||
|
getConfigLink,
|
||||||
|
getFetchIndexResourcesFailure,
|
||||||
|
getGroupsLink,
|
||||||
|
getLogoutLink,
|
||||||
|
getMeLink,
|
||||||
|
getRepositoriesLink,
|
||||||
|
getUsersLink,
|
||||||
|
isFetchIndexResourcesPending
|
||||||
|
} from "../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
me: Me,
|
me: Me,
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
|
repositoriesLink: string,
|
||||||
|
usersLink: string,
|
||||||
|
groupsLink: string,
|
||||||
|
configLink: string,
|
||||||
|
logoutLink: string,
|
||||||
|
meLink: string,
|
||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
fetchMe: () => void,
|
fetchMe: (link: string) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: string => string
|
t: string => string
|
||||||
@@ -36,14 +53,37 @@ type Props = {
|
|||||||
|
|
||||||
class App extends Component<Props> {
|
class App extends Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchMe();
|
if (this.props.meLink) {
|
||||||
|
this.props.fetchMe(this.props.meLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { me, loading, error, authenticated, t } = this.props;
|
const {
|
||||||
|
me,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
authenticated,
|
||||||
|
t,
|
||||||
|
repositoriesLink,
|
||||||
|
usersLink,
|
||||||
|
groupsLink,
|
||||||
|
configLink,
|
||||||
|
logoutLink
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
const navigation = authenticated ? <PrimaryNavigation /> : "";
|
const navigation = authenticated ? (
|
||||||
|
<PrimaryNavigation
|
||||||
|
repositoriesLink={repositoriesLink}
|
||||||
|
usersLink={usersLink}
|
||||||
|
groupsLink={groupsLink}
|
||||||
|
configLink={configLink}
|
||||||
|
logoutLink={logoutLink}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
content = <Loading />;
|
content = <Loading />;
|
||||||
@@ -70,20 +110,34 @@ class App extends Component<Props> {
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch: any) => {
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
return {
|
return {
|
||||||
fetchMe: () => dispatch(fetchMe())
|
fetchMe: (link: string) => dispatch(fetchMe(link))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const authenticated = isAuthenticated(state);
|
const authenticated = isAuthenticated(state);
|
||||||
const me = getMe(state);
|
const me = getMe(state);
|
||||||
const loading = isFetchMePending(state);
|
const loading =
|
||||||
const error = getFetchMeFailure(state);
|
isFetchMePending(state) || isFetchIndexResourcesPending(state);
|
||||||
|
const error =
|
||||||
|
getFetchMeFailure(state) || getFetchIndexResourcesFailure(state);
|
||||||
|
const repositoriesLink = getRepositoriesLink(state);
|
||||||
|
const usersLink = getUsersLink(state);
|
||||||
|
const groupsLink = getGroupsLink(state);
|
||||||
|
const configLink = getConfigLink(state);
|
||||||
|
const logoutLink = getLogoutLink(state);
|
||||||
|
const meLink = getMeLink(state);
|
||||||
return {
|
return {
|
||||||
authenticated,
|
authenticated,
|
||||||
me,
|
me,
|
||||||
loading,
|
loading,
|
||||||
error
|
error,
|
||||||
|
repositoriesLink,
|
||||||
|
usersLink,
|
||||||
|
groupsLink,
|
||||||
|
configLink,
|
||||||
|
logoutLink,
|
||||||
|
meLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
80
scm-ui/src/containers/Index.js
Normal file
80
scm-ui/src/containers/Index.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import App from "./App";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { withRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||||
|
import {
|
||||||
|
fetchIndexResources,
|
||||||
|
getFetchIndexResourcesFailure,
|
||||||
|
getLinks,
|
||||||
|
isFetchIndexResourcesPending
|
||||||
|
} from "../modules/indexResource";
|
||||||
|
import PluginLoader from "./PluginLoader";
|
||||||
|
import type { IndexResources } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error: Error,
|
||||||
|
loading: boolean,
|
||||||
|
indexResources: IndexResources,
|
||||||
|
|
||||||
|
// dispatcher functions
|
||||||
|
fetchIndexResources: () => void,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class Index extends Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchIndexResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { indexResources, loading, error, t } = this.props;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
title={t("app.error.title")}
|
||||||
|
subtitle={t("app.error.subtitle")}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (loading || !indexResources) {
|
||||||
|
return <Loading />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<PluginLoader>
|
||||||
|
<App />
|
||||||
|
</PluginLoader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
fetchIndexResources: () => dispatch(fetchIndexResources())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const loading = isFetchIndexResourcesPending(state);
|
||||||
|
const error = getFetchIndexResourcesFailure(state);
|
||||||
|
const indexResources = getLinks(state);
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
indexResources
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRouter(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(translate("commons")(Index))
|
||||||
|
);
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Image
|
Image
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { getLoginLink } from "../modules/indexResource";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
avatar: {
|
avatar: {
|
||||||
@@ -41,9 +42,10 @@ type Props = {
|
|||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
link: string,
|
||||||
|
|
||||||
// dispatcher props
|
// dispatcher props
|
||||||
login: (username: string, password: string) => void,
|
login: (link: string, username: string, password: string) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -74,7 +76,11 @@ class Login extends React.Component<Props, State> {
|
|||||||
handleSubmit = (event: Event) => {
|
handleSubmit = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.isValid()) {
|
if (this.isValid()) {
|
||||||
this.props.login(this.state.username, this.state.password);
|
this.props.login(
|
||||||
|
this.props.link,
|
||||||
|
this.state.username,
|
||||||
|
this.state.password
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,17 +151,19 @@ const mapStateToProps = state => {
|
|||||||
const authenticated = isAuthenticated(state);
|
const authenticated = isAuthenticated(state);
|
||||||
const loading = isLoginPending(state);
|
const loading = isLoginPending(state);
|
||||||
const error = getLoginFailure(state);
|
const error = getLoginFailure(state);
|
||||||
|
const link = getLoginLink(state);
|
||||||
return {
|
return {
|
||||||
authenticated,
|
authenticated,
|
||||||
loading,
|
loading,
|
||||||
error
|
error,
|
||||||
|
link
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
login: (username: string, password: string) =>
|
login: (loginLink: string, username: string, password: string) =>
|
||||||
dispatch(login(username, password))
|
dispatch(login(loginLink, username, password))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ import {
|
|||||||
getLogoutFailure
|
getLogoutFailure
|
||||||
} from "../modules/auth";
|
} from "../modules/auth";
|
||||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||||
|
import { fetchIndexResources, getLogoutLink } from "../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
logoutLink: string,
|
||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
logout: () => void,
|
logout: (link: string) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: string => string
|
t: string => string
|
||||||
@@ -26,7 +28,7 @@ type Props = {
|
|||||||
|
|
||||||
class Logout extends React.Component<Props> {
|
class Logout extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.logout();
|
this.props.logout(this.props.logoutLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -51,16 +53,18 @@ const mapStateToProps = state => {
|
|||||||
const authenticated = isAuthenticated(state);
|
const authenticated = isAuthenticated(state);
|
||||||
const loading = isLogoutPending(state);
|
const loading = isLogoutPending(state);
|
||||||
const error = getLogoutFailure(state);
|
const error = getLogoutFailure(state);
|
||||||
|
const logoutLink = getLogoutLink(state);
|
||||||
return {
|
return {
|
||||||
authenticated,
|
authenticated,
|
||||||
loading,
|
loading,
|
||||||
error
|
error,
|
||||||
|
logoutLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
logout: () => dispatch(logout())
|
logout: (link: string) => dispatch(logout(link))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { apiClient, Loading } from "@scm-manager/ui-components";
|
import { apiClient, Loading } from "@scm-manager/ui-components";
|
||||||
|
import { getUiPluginsLink } from "../modules/indexResource";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.Node
|
children: React.Node,
|
||||||
|
link: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -29,8 +32,13 @@ class PluginLoader extends React.Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
message: "loading plugin information"
|
message: "loading plugin information"
|
||||||
});
|
});
|
||||||
apiClient
|
|
||||||
.get("ui/plugins")
|
this.getPlugins(this.props.link);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugins = (link: string): Promise<any> => {
|
||||||
|
return apiClient
|
||||||
|
.get(link)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(JSON.parse)
|
.then(JSON.parse)
|
||||||
.then(pluginCollection => pluginCollection._embedded.plugins)
|
.then(pluginCollection => pluginCollection._embedded.plugins)
|
||||||
@@ -40,7 +48,7 @@ class PluginLoader extends React.Component<Props, State> {
|
|||||||
finished: true
|
finished: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
loadPlugins = (plugins: Plugin[]) => {
|
loadPlugins = (plugins: Plugin[]) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -87,4 +95,11 @@ class PluginLoader extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PluginLoader;
|
const mapStateToProps = state => {
|
||||||
|
const link = getUiPluginsLink(state);
|
||||||
|
return {
|
||||||
|
link
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PluginLoader);
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import users from "./users/modules/users";
|
|||||||
import repos from "./repos/modules/repos";
|
import repos from "./repos/modules/repos";
|
||||||
import repositoryTypes from "./repos/modules/repositoryTypes";
|
import repositoryTypes from "./repos/modules/repositoryTypes";
|
||||||
import changesets from "./repos/modules/changesets";
|
import changesets from "./repos/modules/changesets";
|
||||||
|
import sources from "./repos/sources/modules/sources";
|
||||||
import groups from "./groups/modules/groups";
|
import groups from "./groups/modules/groups";
|
||||||
import auth from "./modules/auth";
|
import auth from "./modules/auth";
|
||||||
import pending from "./modules/pending";
|
import pending from "./modules/pending";
|
||||||
import failure from "./modules/failure";
|
import failure from "./modules/failure";
|
||||||
import permissions from "./repos/permissions/modules/permissions";
|
import permissions from "./repos/permissions/modules/permissions";
|
||||||
import config from "./config/modules/config";
|
import config from "./config/modules/config";
|
||||||
|
import indexResources from "./modules/indexResource";
|
||||||
|
|
||||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
import branches from "./repos/modules/branches";
|
import branches from "./repos/modules/branches";
|
||||||
@@ -26,6 +28,7 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
router: routerReducer,
|
router: routerReducer,
|
||||||
pending,
|
pending,
|
||||||
failure,
|
failure,
|
||||||
|
indexResources,
|
||||||
users,
|
users,
|
||||||
repos,
|
repos,
|
||||||
repositoryTypes,
|
repositoryTypes,
|
||||||
@@ -34,7 +37,8 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
permissions,
|
permissions,
|
||||||
groups,
|
groups,
|
||||||
auth,
|
auth,
|
||||||
config
|
config,
|
||||||
|
sources
|
||||||
});
|
});
|
||||||
|
|
||||||
return createStore(
|
return createStore(
|
||||||
|
|||||||
@@ -9,18 +9,21 @@ import {
|
|||||||
createGroup,
|
createGroup,
|
||||||
isCreateGroupPending,
|
isCreateGroupPending,
|
||||||
getCreateGroupFailure,
|
getCreateGroupFailure,
|
||||||
createGroupReset
|
createGroupReset,
|
||||||
|
getCreateGroupLink
|
||||||
} from "../modules/groups";
|
} from "../modules/groups";
|
||||||
import type { Group } from "@scm-manager/ui-types";
|
import type { Group } from "@scm-manager/ui-types";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
|
import { getGroupsLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
t: string => string,
|
t: string => string,
|
||||||
createGroup: (group: Group, callback?: () => void) => void,
|
createGroup: (link: string, group: Group, callback?: () => void) => void,
|
||||||
history: History,
|
history: History,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
error?: Error,
|
error?: Error,
|
||||||
resetForm: () => void
|
resetForm: () => void,
|
||||||
|
createLink: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {};
|
type State = {};
|
||||||
@@ -51,14 +54,14 @@ class AddGroup extends React.Component<Props, State> {
|
|||||||
this.props.history.push("/groups");
|
this.props.history.push("/groups");
|
||||||
};
|
};
|
||||||
createGroup = (group: Group) => {
|
createGroup = (group: Group) => {
|
||||||
this.props.createGroup(group, this.groupCreated);
|
this.props.createGroup(this.props.createLink, group, this.groupCreated);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
createGroup: (group: Group, callback?: () => void) =>
|
createGroup: (link: string, group: Group, callback?: () => void) =>
|
||||||
dispatch(createGroup(group, callback)),
|
dispatch(createGroup(link, group, callback)),
|
||||||
resetForm: () => {
|
resetForm: () => {
|
||||||
dispatch(createGroupReset());
|
dispatch(createGroupReset());
|
||||||
}
|
}
|
||||||
@@ -68,7 +71,9 @@ const mapDispatchToProps = dispatch => {
|
|||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const loading = isCreateGroupPending(state);
|
const loading = isCreateGroupPending(state);
|
||||||
const error = getCreateGroupFailure(state);
|
const error = getCreateGroupFailure(state);
|
||||||
|
const createLink = getGroupsLink(state);
|
||||||
return {
|
return {
|
||||||
|
createLink,
|
||||||
loading,
|
loading,
|
||||||
error
|
error
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
isPermittedToCreateGroups,
|
isPermittedToCreateGroups,
|
||||||
selectListAsCollection
|
selectListAsCollection
|
||||||
} from "../modules/groups";
|
} from "../modules/groups";
|
||||||
|
import { getGroupsLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
groups: Group[],
|
groups: Group[],
|
||||||
@@ -26,19 +27,20 @@ type Props = {
|
|||||||
canAddGroups: boolean,
|
canAddGroups: boolean,
|
||||||
list: PagedCollection,
|
list: PagedCollection,
|
||||||
page: number,
|
page: number,
|
||||||
|
groupLink: string,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
history: History,
|
history: History,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
fetchGroupsByPage: (page: number) => void,
|
fetchGroupsByPage: (link: string, page: number) => void,
|
||||||
fetchGroupsByLink: (link: string) => void
|
fetchGroupsByLink: (link: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
class Groups extends React.Component<Props> {
|
class Groups extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchGroupsByPage(this.props.page);
|
this.props.fetchGroupsByPage(this.props.groupLink, this.props.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange = (link: string) => {
|
onPageChange = (link: string) => {
|
||||||
@@ -111,20 +113,23 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const canAddGroups = isPermittedToCreateGroups(state);
|
const canAddGroups = isPermittedToCreateGroups(state);
|
||||||
const list = selectListAsCollection(state);
|
const list = selectListAsCollection(state);
|
||||||
|
|
||||||
|
const groupLink = getGroupsLink(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups,
|
groups,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
canAddGroups,
|
canAddGroups,
|
||||||
list,
|
list,
|
||||||
page
|
page,
|
||||||
|
groupLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchGroupsByPage: (page: number) => {
|
fetchGroupsByPage: (link: string, page: number) => {
|
||||||
dispatch(fetchGroupsByPage(page));
|
dispatch(fetchGroupsByPage(link, page));
|
||||||
},
|
},
|
||||||
fetchGroupsByLink: (link: string) => {
|
fetchGroupsByLink: (link: string) => {
|
||||||
dispatch(fetchGroupsByLink(link));
|
dispatch(fetchGroupsByLink(link));
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ import {
|
|||||||
|
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import EditGroup from "./EditGroup";
|
import EditGroup from "./EditGroup";
|
||||||
|
import { getGroupsLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
group: Group,
|
group: Group,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
groupLink: string,
|
||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
deleteGroup: (group: Group, callback?: () => void) => void,
|
deleteGroup: (group: Group, callback?: () => void) => void,
|
||||||
fetchGroup: string => void,
|
fetchGroup: (string, string) => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -45,7 +47,7 @@ type Props = {
|
|||||||
|
|
||||||
class SingleGroup extends React.Component<Props> {
|
class SingleGroup extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchGroup(this.props.name);
|
this.props.fetchGroup(this.props.groupLink, this.props.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
stripEndingSlash = (url: string) => {
|
stripEndingSlash = (url: string) => {
|
||||||
@@ -132,19 +134,21 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
isFetchGroupPending(state, name) || isDeleteGroupPending(state, name);
|
isFetchGroupPending(state, name) || isDeleteGroupPending(state, name);
|
||||||
const error =
|
const error =
|
||||||
getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name);
|
getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name);
|
||||||
|
const groupLink = getGroupsLink(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
group,
|
group,
|
||||||
loading,
|
loading,
|
||||||
error
|
error,
|
||||||
|
groupLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchGroup: (name: string) => {
|
fetchGroup: (link: string, name: string) => {
|
||||||
dispatch(fetchGroup(name));
|
dispatch(fetchGroup(link, name));
|
||||||
},
|
},
|
||||||
deleteGroup: (group: Group, callback?: () => void) => {
|
deleteGroup: (group: Group, callback?: () => void) => {
|
||||||
dispatch(deleteGroup(group, callback));
|
dispatch(deleteGroup(group, callback));
|
||||||
|
|||||||
@@ -32,17 +32,16 @@ export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`;
|
|||||||
export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`;
|
export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`;
|
export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
const GROUPS_URL = "groups";
|
|
||||||
const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2";
|
const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2";
|
||||||
|
|
||||||
// fetch groups
|
// fetch groups
|
||||||
export function fetchGroups() {
|
export function fetchGroups(link: string) {
|
||||||
return fetchGroupsByLink(GROUPS_URL);
|
return fetchGroupsByLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchGroupsByPage(page: number) {
|
export function fetchGroupsByPage(link: string, page: number) {
|
||||||
// backend start counting by 0
|
// backend start counting by 0
|
||||||
return fetchGroupsByLink(GROUPS_URL + "?page=" + (page - 1));
|
return fetchGroupsByLink(link + "?page=" + (page - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchGroupsByLink(link: string) {
|
export function fetchGroupsByLink(link: string) {
|
||||||
@@ -56,7 +55,7 @@ export function fetchGroupsByLink(link: string) {
|
|||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
const error = new Error(`could not fetch groups: ${cause.message}`);
|
const error = new Error(`could not fetch groups: ${cause.message}`);
|
||||||
dispatch(fetchGroupsFailure(GROUPS_URL, error));
|
dispatch(fetchGroupsFailure(link, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -85,8 +84,8 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//fetch group
|
//fetch group
|
||||||
export function fetchGroup(name: string) {
|
export function fetchGroup(link: string, name: string) {
|
||||||
const groupUrl = GROUPS_URL + "/" + name;
|
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchGroupPending(name));
|
dispatch(fetchGroupPending(name));
|
||||||
return apiClient
|
return apiClient
|
||||||
@@ -132,11 +131,11 @@ export function fetchGroupFailure(name: string, error: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//create group
|
//create group
|
||||||
export function createGroup(group: Group, callback?: () => void) {
|
export function createGroup(link: string, group: Group, callback?: () => void) {
|
||||||
return function(dispatch: Dispatch) {
|
return function(dispatch: Dispatch) {
|
||||||
dispatch(createGroupPending());
|
dispatch(createGroupPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(GROUPS_URL, group, CONTENT_TYPE_GROUP)
|
.post(link, group, CONTENT_TYPE_GROUP)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(createGroupSuccess());
|
dispatch(createGroupSuccess());
|
||||||
if (callback) {
|
if (callback) {
|
||||||
@@ -410,6 +409,12 @@ export const isPermittedToCreateGroups = (state: Object): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getCreateGroupLink(state: Object) {
|
||||||
|
if (state.groups.list.entry && state.groups.list.entry._links)
|
||||||
|
return state.groups.list.entry._links.create.href;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getGroupsFromState(state: Object) {
|
export function getGroupsFromState(state: Object) {
|
||||||
const groupNames = selectList(state).entries;
|
const groupNames = selectList(state).entries;
|
||||||
if (!groupNames) {
|
if (!groupNames) {
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ import reducer, {
|
|||||||
modifyGroup,
|
modifyGroup,
|
||||||
MODIFY_GROUP_PENDING,
|
MODIFY_GROUP_PENDING,
|
||||||
MODIFY_GROUP_SUCCESS,
|
MODIFY_GROUP_SUCCESS,
|
||||||
MODIFY_GROUP_FAILURE
|
MODIFY_GROUP_FAILURE,
|
||||||
|
getCreateGroupLink
|
||||||
} from "./groups";
|
} from "./groups";
|
||||||
const GROUPS_URL = "/api/v2/groups";
|
const GROUPS_URL = "/api/v2/groups";
|
||||||
|
const URL = "/groups";
|
||||||
|
|
||||||
const error = new Error("You have an error!");
|
const error = new Error("You have an error!");
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ const humanGroup = {
|
|||||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
href:"http://localhost:8081/api/v2/groups/humanGroup"
|
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_embedded: {
|
_embedded: {
|
||||||
@@ -95,7 +97,7 @@ const emptyGroup = {
|
|||||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
href:"http://localhost:8081/api/v2/groups/emptyGroup"
|
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_embedded: {
|
_embedded: {
|
||||||
@@ -150,7 +152,7 @@ describe("groups fetch()", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(fetchGroups()).then(() => {
|
return store.dispatch(fetchGroups(URL)).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -161,7 +163,7 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchGroups()).then(() => {
|
return store.dispatch(fetchGroups(URL)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_GROUPS_PENDING);
|
expect(actions[0].type).toEqual(FETCH_GROUPS_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_GROUPS_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_GROUPS_FAILURE);
|
||||||
@@ -173,7 +175,7 @@ describe("groups fetch()", () => {
|
|||||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchGroup("humanGroup")).then(() => {
|
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||||
@@ -187,7 +189,7 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchGroup("humanGroup")).then(() => {
|
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||||
@@ -195,14 +197,13 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should successfully create group", () => {
|
it("should successfully create group", () => {
|
||||||
fetchMock.postOnce(GROUPS_URL, {
|
fetchMock.postOnce(GROUPS_URL, {
|
||||||
status: 201
|
status: 201
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
return store.dispatch(createGroup(URL, humanGroup)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||||
@@ -219,7 +220,7 @@ describe("groups fetch()", () => {
|
|||||||
called = true;
|
called = true;
|
||||||
};
|
};
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createGroup(humanGroup, callMe)).then(() => {
|
return store.dispatch(createGroup(URL, humanGroup, callMe)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||||
@@ -227,14 +228,13 @@ describe("groups fetch()", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should fail creating group on HTTP 500", () => {
|
it("should fail creating group on HTTP 500", () => {
|
||||||
fetchMock.postOnce(GROUPS_URL, {
|
fetchMock.postOnce(GROUPS_URL, {
|
||||||
status: 500
|
status: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
return store.dispatch(createGroup(URL, humanGroup)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE);
|
expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE);
|
||||||
@@ -337,13 +337,10 @@ describe("groups fetch()", () => {
|
|||||||
expect(actions[1].payload).toBeDefined();
|
expect(actions[1].payload).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("groups reducer", () => {
|
describe("groups reducer", () => {
|
||||||
|
|
||||||
it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => {
|
it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => {
|
||||||
|
|
||||||
const newState = reducer({}, fetchGroupsSuccess(responseBody));
|
const newState = reducer({}, fetchGroupsSuccess(responseBody));
|
||||||
|
|
||||||
expect(newState.list).toEqual({
|
expect(newState.list).toEqual({
|
||||||
@@ -391,7 +388,6 @@ describe("groups reducer", () => {
|
|||||||
expect(newState.byNames["humanGroup"]).toBeTruthy();
|
expect(newState.byNames["humanGroup"]).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should update state according to FETCH_GROUP_SUCCESS action", () => {
|
it("should update state according to FETCH_GROUP_SUCCESS action", () => {
|
||||||
const newState = reducer({}, fetchGroupSuccess(emptyGroup));
|
const newState = reducer({}, fetchGroupSuccess(emptyGroup));
|
||||||
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
|
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
|
||||||
@@ -426,7 +422,6 @@ describe("groups reducer", () => {
|
|||||||
expect(newState.byNames["emptyGroup"]).toBeFalsy();
|
expect(newState.byNames["emptyGroup"]).toBeFalsy();
|
||||||
expect(newState.list.entries).toEqual(["humanGroup"]);
|
expect(newState.list.entries).toEqual(["humanGroup"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("selector tests", () => {
|
describe("selector tests", () => {
|
||||||
@@ -476,6 +471,23 @@ describe("selector tests", () => {
|
|||||||
expect(isPermittedToCreateGroups(state)).toBe(true);
|
expect(isPermittedToCreateGroups(state)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return create Group link", () => {
|
||||||
|
const state = {
|
||||||
|
groups: {
|
||||||
|
list: {
|
||||||
|
entry: {
|
||||||
|
_links: {
|
||||||
|
create: {
|
||||||
|
href: "/create"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getCreateGroupLink(state)).toBe("/create");
|
||||||
|
});
|
||||||
|
|
||||||
it("should get groups from state", () => {
|
it("should get groups from state", () => {
|
||||||
const state = {
|
const state = {
|
||||||
groups: {
|
groups: {
|
||||||
@@ -560,9 +572,13 @@ describe("selector tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true if create group is pending", () => {
|
it("should return true if create group is pending", () => {
|
||||||
expect(isCreateGroupPending({pending: {
|
expect(
|
||||||
|
isCreateGroupPending({
|
||||||
|
pending: {
|
||||||
[CREATE_GROUP]: true
|
[CREATE_GROUP]: true
|
||||||
}})).toBeTruthy();
|
}
|
||||||
|
})
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if create group is not pending", () => {
|
it("should return false if create group is not pending", () => {
|
||||||
@@ -570,18 +586,19 @@ describe("selector tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return error if creating group failed", () => {
|
it("should return error if creating group failed", () => {
|
||||||
expect(getCreateGroupFailure({
|
expect(
|
||||||
|
getCreateGroupFailure({
|
||||||
failure: {
|
failure: {
|
||||||
[CREATE_GROUP]: error
|
[CREATE_GROUP]: error
|
||||||
}
|
}
|
||||||
})).toEqual(error);
|
})
|
||||||
|
).toEqual(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return undefined if creating group did not fail", () => {
|
it("should return undefined if creating group did not fail", () => {
|
||||||
expect(getCreateGroupFailure({})).toBeUndefined();
|
expect(getCreateGroupFailure({})).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should return true, when delete group humanGroup is pending", () => {
|
it("should return true, when delete group humanGroup is pending", () => {
|
||||||
const state = {
|
const state = {
|
||||||
pending: {
|
pending: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import App from "./containers/App";
|
import Index from "./containers/Index";
|
||||||
import registerServiceWorker from "./registerServiceWorker";
|
import registerServiceWorker from "./registerServiceWorker";
|
||||||
|
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
@@ -37,9 +37,7 @@ ReactDOM.render(
|
|||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
{/* ConnectedRouter will use the store from Provider automatically */}
|
{/* ConnectedRouter will use the store from Provider automatically */}
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<PluginLoader>
|
<Index />
|
||||||
<App />
|
|
||||||
</PluginLoader>
|
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import * as types from "./types";
|
|||||||
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
||||||
import { isPending } from "./pending";
|
import { isPending } from "./pending";
|
||||||
import { getFailure } from "./failure";
|
import { getFailure } from "./failure";
|
||||||
|
import {
|
||||||
|
callFetchIndexResources,
|
||||||
|
FETCH_INDEXRESOURCES_SUCCESS,
|
||||||
|
fetchIndexResources, fetchIndexResourcesPending,
|
||||||
|
fetchIndexResourcesSuccess
|
||||||
|
} from "./indexResource";
|
||||||
|
|
||||||
// Action
|
// Action
|
||||||
|
|
||||||
@@ -121,16 +127,11 @@ export const fetchMeFailure = (error: Error) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// urls
|
|
||||||
|
|
||||||
const ME_URL = "/me";
|
|
||||||
const LOGIN_URL = "/auth/access_token";
|
|
||||||
|
|
||||||
// side effects
|
// side effects
|
||||||
|
|
||||||
const callFetchMe = (): Promise<Me> => {
|
const callFetchMe = (link: string): Promise<Me> => {
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(ME_URL)
|
.get(link)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
@@ -139,7 +140,11 @@ const callFetchMe = (): Promise<Me> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const login = (username: string, password: string) => {
|
export const login = (
|
||||||
|
loginLink: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
) => {
|
||||||
const login_data = {
|
const login_data = {
|
||||||
cookie: true,
|
cookie: true,
|
||||||
grant_type: "password",
|
grant_type: "password",
|
||||||
@@ -149,9 +154,15 @@ export const login = (username: string, password: string) => {
|
|||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(loginPending());
|
dispatch(loginPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(LOGIN_URL, login_data)
|
.post(loginLink, login_data)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return callFetchMe();
|
dispatch(fetchIndexResourcesPending())
|
||||||
|
return callFetchIndexResources();
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
dispatch(fetchIndexResourcesSuccess(response));
|
||||||
|
const meLink = response._links.me.href;
|
||||||
|
return callFetchMe(meLink);
|
||||||
})
|
})
|
||||||
.then(me => {
|
.then(me => {
|
||||||
dispatch(loginSuccess(me));
|
dispatch(loginSuccess(me));
|
||||||
@@ -162,10 +173,10 @@ export const login = (username: string, password: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchMe = () => {
|
export const fetchMe = (link: string) => {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchMePending());
|
dispatch(fetchMePending());
|
||||||
return callFetchMe()
|
return callFetchMe(link)
|
||||||
.then(me => {
|
.then(me => {
|
||||||
dispatch(fetchMeSuccess(me));
|
dispatch(fetchMeSuccess(me));
|
||||||
})
|
})
|
||||||
@@ -179,14 +190,17 @@ export const fetchMe = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logout = () => {
|
export const logout = (link: string) => {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(logoutPending());
|
dispatch(logoutPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.delete(LOGIN_URL)
|
.delete(link)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(logoutSuccess());
|
dispatch(logoutSuccess());
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(fetchIndexResources());
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch(logoutFailure(error));
|
dispatch(logoutFailure(error));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ import reducer, {
|
|||||||
import configureMockStore from "redux-mock-store";
|
import configureMockStore from "redux-mock-store";
|
||||||
import thunk from "redux-thunk";
|
import thunk from "redux-thunk";
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
|
import {
|
||||||
|
FETCH_INDEXRESOURCES_PENDING,
|
||||||
|
FETCH_INDEXRESOURCES_SUCCESS
|
||||||
|
} from "./indexResource";
|
||||||
|
|
||||||
const me = { name: "tricia", displayName: "Tricia McMillian" };
|
const me = { name: "tricia", displayName: "Tricia McMillian" };
|
||||||
|
|
||||||
@@ -93,14 +97,28 @@ describe("auth actions", () => {
|
|||||||
headers: { "content-type": "application/json" }
|
headers: { "content-type": "application/json" }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const meLink = {
|
||||||
|
me: {
|
||||||
|
href: "/me"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMock.getOnce("/api/v2/", {
|
||||||
|
_links: meLink
|
||||||
|
});
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: LOGIN_PENDING },
|
{ type: LOGIN_PENDING },
|
||||||
|
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||||
|
{ type: FETCH_INDEXRESOURCES_SUCCESS, payload: { _links: meLink } },
|
||||||
{ type: LOGIN_SUCCESS, payload: me }
|
{ type: LOGIN_SUCCESS, payload: me }
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(login("tricia", "secret123")).then(() => {
|
return store
|
||||||
|
.dispatch(login("/auth/access_token", "tricia", "secret123"))
|
||||||
|
.then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -111,7 +129,9 @@ describe("auth actions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(login("tricia", "secret123")).then(() => {
|
return store
|
||||||
|
.dispatch(login("/auth/access_token", "tricia", "secret123"))
|
||||||
|
.then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(LOGIN_PENDING);
|
expect(actions[0].type).toEqual(LOGIN_PENDING);
|
||||||
expect(actions[1].type).toEqual(LOGIN_FAILURE);
|
expect(actions[1].type).toEqual(LOGIN_FAILURE);
|
||||||
@@ -135,7 +155,7 @@ describe("auth actions", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(fetchMe()).then(() => {
|
return store.dispatch(fetchMe("me")).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -146,7 +166,7 @@ describe("auth actions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchMe()).then(() => {
|
return store.dispatch(fetchMe("me")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_ME_PENDING);
|
expect(actions[0].type).toEqual(FETCH_ME_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
|
||||||
@@ -166,7 +186,7 @@ describe("auth actions", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(fetchMe()).then(() => {
|
return store.dispatch(fetchMe("me")).then(() => {
|
||||||
// return of async actions
|
// return of async actions
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
@@ -181,14 +201,23 @@ describe("auth actions", () => {
|
|||||||
status: 401
|
status: 401
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fetchMock.getOnce("/api/v2/", {
|
||||||
|
_links: {
|
||||||
|
login: {
|
||||||
|
login: "/login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: LOGOUT_PENDING },
|
{ type: LOGOUT_PENDING },
|
||||||
{ type: LOGOUT_SUCCESS }
|
{ type: LOGOUT_SUCCESS },
|
||||||
|
{ type: FETCH_INDEXRESOURCES_PENDING }
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(logout()).then(() => {
|
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -199,7 +228,7 @@ describe("auth actions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(logout()).then(() => {
|
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(LOGOUT_PENDING);
|
expect(actions[0].type).toEqual(LOGOUT_PENDING);
|
||||||
expect(actions[1].type).toEqual(LOGOUT_FAILURE);
|
expect(actions[1].type).toEqual(LOGOUT_FAILURE);
|
||||||
|
|||||||
145
scm-ui/src/modules/indexResource.js
Normal file
145
scm-ui/src/modules/indexResource.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// @flow
|
||||||
|
import * as types from "./types";
|
||||||
|
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
import type { Action, IndexResources } from "@scm-manager/ui-types";
|
||||||
|
import { isPending } from "./pending";
|
||||||
|
import { getFailure } from "./failure";
|
||||||
|
|
||||||
|
// Action
|
||||||
|
|
||||||
|
export const FETCH_INDEXRESOURCES = "scm/INDEXRESOURCES";
|
||||||
|
export const FETCH_INDEXRESOURCES_PENDING = `${FETCH_INDEXRESOURCES}_${
|
||||||
|
types.PENDING_SUFFIX
|
||||||
|
}`;
|
||||||
|
export const FETCH_INDEXRESOURCES_SUCCESS = `${FETCH_INDEXRESOURCES}_${
|
||||||
|
types.SUCCESS_SUFFIX
|
||||||
|
}`;
|
||||||
|
export const FETCH_INDEXRESOURCES_FAILURE = `${FETCH_INDEXRESOURCES}_${
|
||||||
|
types.FAILURE_SUFFIX
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const INDEX_RESOURCES_LINK = "/";
|
||||||
|
|
||||||
|
export const callFetchIndexResources = (): Promise<IndexResources> => {
|
||||||
|
return apiClient.get(INDEX_RESOURCES_LINK).then(response => {
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchIndexResources() {
|
||||||
|
return function(dispatch: any) {
|
||||||
|
dispatch(fetchIndexResourcesPending());
|
||||||
|
return callFetchIndexResources()
|
||||||
|
.then(resources => {
|
||||||
|
dispatch(fetchIndexResourcesSuccess(resources));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(fetchIndexResourcesFailure(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchIndexResourcesPending(): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_INDEXRESOURCES_PENDING
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchIndexResourcesSuccess(resources: IndexResources): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_INDEXRESOURCES_SUCCESS,
|
||||||
|
payload: resources
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchIndexResourcesFailure(err: Error): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_INDEXRESOURCES_FAILURE,
|
||||||
|
payload: err
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// reducer
|
||||||
|
export default function reducer(
|
||||||
|
state: Object = {},
|
||||||
|
action: Action = { type: "UNKNOWN" }
|
||||||
|
): Object {
|
||||||
|
if (!action.payload) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case FETCH_INDEXRESOURCES_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
links: action.payload._links
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
|
||||||
|
export function isFetchIndexResourcesPending(state: Object) {
|
||||||
|
return isPending(state, FETCH_INDEXRESOURCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchIndexResourcesFailure(state: Object) {
|
||||||
|
return getFailure(state, FETCH_INDEXRESOURCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLinks(state: Object) {
|
||||||
|
return state.indexResources.links;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLink(state: Object, name: string) {
|
||||||
|
if (state.indexResources.links && state.indexResources.links[name]) {
|
||||||
|
return state.indexResources.links[name].href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUiPluginsLink(state: Object) {
|
||||||
|
return getLink(state, "uiPlugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMeLink(state: Object) {
|
||||||
|
return getLink(state, "me");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogoutLink(state: Object) {
|
||||||
|
return getLink(state, "logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLoginLink(state: Object) {
|
||||||
|
return getLink(state, "login");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsersLink(state: Object) {
|
||||||
|
return getLink(state, "users");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGroupsLink(state: Object) {
|
||||||
|
return getLink(state, "groups");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigLink(state: Object) {
|
||||||
|
return getLink(state, "config");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRepositoriesLink(state: Object) {
|
||||||
|
return getLink(state, "repositories");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHgConfigLink(state: Object) {
|
||||||
|
return getLink(state, "hgConfig");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGitConfigLink(state: Object) {
|
||||||
|
return getLink(state, "gitConfig");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSvnConfigLink(state: Object) {
|
||||||
|
return getLink(state, "svnConfig");
|
||||||
|
}
|
||||||
426
scm-ui/src/modules/indexResource.test.js
Normal file
426
scm-ui/src/modules/indexResource.test.js
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
import configureMockStore from "redux-mock-store";
|
||||||
|
import thunk from "redux-thunk";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import reducer, {
|
||||||
|
FETCH_INDEXRESOURCES_PENDING,
|
||||||
|
FETCH_INDEXRESOURCES_SUCCESS,
|
||||||
|
FETCH_INDEXRESOURCES_FAILURE,
|
||||||
|
fetchIndexResources,
|
||||||
|
fetchIndexResourcesSuccess,
|
||||||
|
FETCH_INDEXRESOURCES,
|
||||||
|
isFetchIndexResourcesPending,
|
||||||
|
getFetchIndexResourcesFailure,
|
||||||
|
getUiPluginsLink,
|
||||||
|
getMeLink,
|
||||||
|
getLogoutLink,
|
||||||
|
getLoginLink,
|
||||||
|
getUsersLink,
|
||||||
|
getConfigLink,
|
||||||
|
getRepositoriesLink,
|
||||||
|
getHgConfigLink,
|
||||||
|
getGitConfigLink,
|
||||||
|
getSvnConfigLink,
|
||||||
|
getLinks, getGroupsLink
|
||||||
|
} from "./indexResource";
|
||||||
|
|
||||||
|
const indexResourcesUnauthenticated = {
|
||||||
|
version: "2.0.0-SNAPSHOT",
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/"
|
||||||
|
},
|
||||||
|
uiPlugins: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/ui/plugins"
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/auth/access_token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const indexResourcesAuthenticated = {
|
||||||
|
version: "2.0.0-SNAPSHOT",
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/"
|
||||||
|
},
|
||||||
|
uiPlugins: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/ui/plugins"
|
||||||
|
},
|
||||||
|
me: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/me/"
|
||||||
|
},
|
||||||
|
logout: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/auth/access_token"
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/users/"
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/groups/"
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/config"
|
||||||
|
},
|
||||||
|
repositories: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/repositories/"
|
||||||
|
},
|
||||||
|
hgConfig: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/config/hg"
|
||||||
|
},
|
||||||
|
gitConfig: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/config/git"
|
||||||
|
},
|
||||||
|
svnConfig: {
|
||||||
|
href: "http://localhost:8081/scm/api/v2/config/svn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("fetch index resource", () => {
|
||||||
|
const index_url = "/api/v2/";
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch index resources when unauthenticated", () => {
|
||||||
|
fetchMock.getOnce(index_url, indexResourcesUnauthenticated);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||||
|
{
|
||||||
|
type: FETCH_INDEXRESOURCES_SUCCESS,
|
||||||
|
payload: indexResourcesUnauthenticated
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchIndexResources()).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch index resources when authenticated", () => {
|
||||||
|
fetchMock.getOnce(index_url, indexResourcesAuthenticated);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||||
|
{
|
||||||
|
type: FETCH_INDEXRESOURCES_SUCCESS,
|
||||||
|
payload: indexResourcesAuthenticated
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchIndexResources()).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch FETCH_INDEX_RESOURCES_FAILURE if request fails", () => {
|
||||||
|
fetchMock.getOnce(index_url, {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchIndexResources()).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_INDEXRESOURCES_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_INDEXRESOURCES_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("index resources reducer", () => {
|
||||||
|
it("should return empty object, if state and action is undefined", () => {
|
||||||
|
expect(reducer()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same state, if the action is undefined", () => {
|
||||||
|
const state = { x: true };
|
||||||
|
expect(reducer(state)).toBe(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same state, if the action is unknown to the reducer", () => {
|
||||||
|
const state = { x: true };
|
||||||
|
expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store the index resources on FETCH_INDEXRESOURCES_SUCCESS", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{},
|
||||||
|
fetchIndexResourcesSuccess(indexResourcesAuthenticated)
|
||||||
|
);
|
||||||
|
expect(newState.links).toBe(indexResourcesAuthenticated._links);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("index resources selectors", () => {
|
||||||
|
const error = new Error("something goes wrong");
|
||||||
|
|
||||||
|
it("should return true, when fetch index resources is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[FETCH_INDEXRESOURCES]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isFetchIndexResourcesPending(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when fetch index resources is not pending", () => {
|
||||||
|
expect(isFetchIndexResourcesPending({})).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return error when fetch index resources did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[FETCH_INDEXRESOURCES]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getFetchIndexResourcesFailure(state)).toEqual(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when fetch index resources did not fail", () => {
|
||||||
|
expect(getFetchIndexResourcesFailure({})).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return all links", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getLinks(state)).toBe(indexResourcesAuthenticated._links);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ui plugins link
|
||||||
|
it("should return ui plugins link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getUiPluginsLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/ui/plugins"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ui plugins links when unauthenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getUiPluginsLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/ui/plugins"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// me link
|
||||||
|
it("should return me link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getMeLink(state)).toBe("http://localhost:8081/scm/api/v2/me/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for me link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getMeLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// logout link
|
||||||
|
it("should return logout link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getLogoutLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/auth/access_token"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for logout link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getLogoutLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// login link
|
||||||
|
it("should return login link when unauthenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getLoginLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/auth/access_token"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for login link when authenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getLoginLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// users link
|
||||||
|
it("should return users link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getUsersLink(state)).toBe("http://localhost:8081/scm/api/v2/users/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for users link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getUsersLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// groups link
|
||||||
|
it("should return groups link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getGroupsLink(state)).toBe("http://localhost:8081/scm/api/v2/groups/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for groups link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getGroupsLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// config link
|
||||||
|
it("should return config link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getConfigLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/config"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for config link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getConfigLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// repositories link
|
||||||
|
it("should return repositories link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getRepositoriesLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/repositories/"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return config for repositories link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getRepositoriesLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// hgConfig link
|
||||||
|
it("should return hgConfig link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getHgConfigLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/config/hg"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return config for hgConfig link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getHgConfigLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// gitConfig link
|
||||||
|
it("should return gitConfig link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getGitConfigLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/config/git"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return config for gitConfig link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getGitConfigLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// svnConfig link
|
||||||
|
it("should return svnConfig link when authenticated and has permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesAuthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getSvnConfigLink(state)).toBe(
|
||||||
|
"http://localhost:8081/scm/api/v2/config/svn"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return config for svnConfig link when unauthenticated or has not permission to see it", () => {
|
||||||
|
const state = {
|
||||||
|
indexResources: {
|
||||||
|
links: indexResourcesUnauthenticated._links
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getSvnConfigLink(state)).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
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,18 +4,18 @@ export type Description = {
|
|||||||
message: string
|
message: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseDescription(description: string): Description {
|
export function parseDescription(description?: string): Description {
|
||||||
let title = "";
|
const desc = description ? description : "";
|
||||||
|
const lineBreak = desc.indexOf("\n");
|
||||||
|
|
||||||
|
let title;
|
||||||
let message = "";
|
let message = "";
|
||||||
|
|
||||||
if (description != null) {
|
|
||||||
const lineBreak = description.indexOf("\n");
|
|
||||||
if (lineBreak > 0) {
|
if (lineBreak > 0) {
|
||||||
title = description.substring(0, lineBreak);
|
title = desc.substring(0, lineBreak);
|
||||||
message = description.substring(lineBreak + 1);
|
message = desc.substring(lineBreak + 1);
|
||||||
} else {
|
} else {
|
||||||
title = description;
|
title = desc;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,4 +13,10 @@ describe("parseDescription tests", () => {
|
|||||||
const desc = parseDescription("Hello Trillian");
|
const desc = parseDescription("Hello Trillian");
|
||||||
expect(desc.title).toBe("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 = {
|
type Props = {
|
||||||
branches: Branch[], // TODO: Use generics?
|
branches: Branch[], // TODO: Use generics?
|
||||||
selected: (branch?: Branch) => void,
|
selected: (branch?: Branch) => void,
|
||||||
|
selectedBranch: string,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
classes: Object,
|
classes: Object,
|
||||||
@@ -31,6 +32,12 @@ class BranchSelector extends React.Component<Props, State> {
|
|||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.branches
|
||||||
|
.filter(branch => branch.name === this.props.selectedBranch)
|
||||||
|
.forEach(branch => this.setState({ selectedBranch: branch }));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { branches, classes, t } = this.props;
|
const { branches, classes, t } = this.props;
|
||||||
|
|
||||||
@@ -60,6 +67,8 @@ class BranchSelector extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,11 +92,12 @@ class BranchRoot extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBranchSelector = () => {
|
renderBranchSelector = () => {
|
||||||
const { repository, branches } = this.props;
|
const { repository, branches, selected } = this.props;
|
||||||
if (repository._links.branches) {
|
if (repository._links.branches) {
|
||||||
return (
|
return (
|
||||||
<BranchSelector
|
<BranchSelector
|
||||||
branches={branches}
|
branches={branches}
|
||||||
|
selectedBranch={selected}
|
||||||
selected={(b: Branch) => {
|
selected={(b: Branch) => {
|
||||||
this.branchSelected(b);
|
this.branchSelected(b);
|
||||||
}}
|
}}
|
||||||
@@ -18,16 +18,18 @@ import {
|
|||||||
isCreateRepoPending
|
isCreateRepoPending
|
||||||
} from "../modules/repos";
|
} from "../modules/repos";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
|
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repositoryTypes: RepositoryType[],
|
repositoryTypes: RepositoryType[],
|
||||||
typesLoading: boolean,
|
typesLoading: boolean,
|
||||||
createLoading: boolean,
|
createLoading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
repoLink: string,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
fetchRepositoryTypesIfNeeded: () => void,
|
fetchRepositoryTypesIfNeeded: () => void,
|
||||||
createRepo: (Repository, callback: () => void) => void,
|
createRepo: (link: string, Repository, callback: () => void) => void,
|
||||||
resetForm: () => void,
|
resetForm: () => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
@@ -55,7 +57,7 @@ class Create extends React.Component<Props> {
|
|||||||
error
|
error
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { t } = this.props;
|
const { t, repoLink } = this.props;
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
title={t("create.title")}
|
title={t("create.title")}
|
||||||
@@ -68,7 +70,7 @@ class Create extends React.Component<Props> {
|
|||||||
repositoryTypes={repositoryTypes}
|
repositoryTypes={repositoryTypes}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
submitForm={repo => {
|
submitForm={repo => {
|
||||||
createRepo(repo, this.repoCreated);
|
createRepo(repoLink, repo, this.repoCreated);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
@@ -82,11 +84,13 @@ const mapStateToProps = state => {
|
|||||||
const createLoading = isCreateRepoPending(state);
|
const createLoading = isCreateRepoPending(state);
|
||||||
const error =
|
const error =
|
||||||
getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state);
|
getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state);
|
||||||
|
const repoLink = getRepositoriesLink(state);
|
||||||
return {
|
return {
|
||||||
repositoryTypes,
|
repositoryTypes,
|
||||||
typesLoading,
|
typesLoading,
|
||||||
createLoading,
|
createLoading,
|
||||||
error
|
error,
|
||||||
|
repoLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,8 +99,12 @@ const mapDispatchToProps = dispatch => {
|
|||||||
fetchRepositoryTypesIfNeeded: () => {
|
fetchRepositoryTypesIfNeeded: () => {
|
||||||
dispatch(fetchRepositoryTypesIfNeeded());
|
dispatch(fetchRepositoryTypesIfNeeded());
|
||||||
},
|
},
|
||||||
createRepo: (repository: Repository, callback: () => void) => {
|
createRepo: (
|
||||||
dispatch(createRepo(repository, callback));
|
link: string,
|
||||||
|
repository: Repository,
|
||||||
|
callback: () => void
|
||||||
|
) => {
|
||||||
|
dispatch(createRepo(link, repository, callback));
|
||||||
},
|
},
|
||||||
resetForm: () => {
|
resetForm: () => {
|
||||||
dispatch(createRepoReset());
|
dispatch(createRepoReset());
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { CreateButton, Page, Paginator } from "@scm-manager/ui-components";
|
|||||||
import RepositoryList from "../components/list";
|
import RepositoryList from "../components/list";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
|
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
page: number,
|
page: number,
|
||||||
@@ -25,10 +26,11 @@ type Props = {
|
|||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
showCreateButton: boolean,
|
showCreateButton: boolean,
|
||||||
|
reposLink: string,
|
||||||
|
|
||||||
// dispatched functions
|
// dispatched functions
|
||||||
fetchRepos: () => void,
|
fetchRepos: string => void,
|
||||||
fetchReposByPage: number => void,
|
fetchReposByPage: (string, number) => void,
|
||||||
fetchReposByLink: string => void,
|
fetchReposByLink: string => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
@@ -38,7 +40,7 @@ type Props = {
|
|||||||
|
|
||||||
class Overview extends React.Component<Props> {
|
class Overview extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchReposByPage(this.props.page);
|
this.props.fetchReposByPage(this.props.reposLink, this.props.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +115,9 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const loading = isFetchReposPending(state);
|
const loading = isFetchReposPending(state);
|
||||||
const error = getFetchReposFailure(state);
|
const error = getFetchReposFailure(state);
|
||||||
const showCreateButton = isAbleToCreateRepos(state);
|
const showCreateButton = isAbleToCreateRepos(state);
|
||||||
|
const reposLink = getRepositoriesLink(state);
|
||||||
return {
|
return {
|
||||||
|
reposLink,
|
||||||
page,
|
page,
|
||||||
collection,
|
collection,
|
||||||
loading,
|
loading,
|
||||||
@@ -124,11 +128,11 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchRepos: () => {
|
fetchRepos: (link: string) => {
|
||||||
dispatch(fetchRepos());
|
dispatch(fetchRepos(link));
|
||||||
},
|
},
|
||||||
fetchReposByPage: (page: number) => {
|
fetchReposByPage: (link: string, page: number) => {
|
||||||
dispatch(fetchReposByPage(page));
|
dispatch(fetchReposByPage(link, page));
|
||||||
},
|
},
|
||||||
fetchReposByLink: (link: string) => {
|
fetchReposByLink: (link: string) => {
|
||||||
dispatch(fetchReposByLink(link));
|
dispatch(fetchReposByLink(link));
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import Permissions from "../permissions/containers/Permissions";
|
|||||||
import type {History} from "history";
|
import type {History} from "history";
|
||||||
import EditNavLink from "../components/EditNavLink";
|
import EditNavLink from "../components/EditNavLink";
|
||||||
|
|
||||||
import BranchRoot from "./BranchRoot";
|
import BranchRoot from "./ChangesetsRoot";
|
||||||
import ChangesetView from "./ChangesetView";
|
import ChangesetView from "./ChangesetView";
|
||||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||||
|
import Sources from "../sources/containers/Sources";
|
||||||
|
import RepositoryNavLink from "../components/RepositoryNavLink";
|
||||||
|
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespace: string,
|
namespace: string,
|
||||||
@@ -26,9 +29,10 @@ type Props = {
|
|||||||
repository: Repository,
|
repository: Repository,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
repoLink: string,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
fetchRepo: (namespace: string, name: string) => void,
|
fetchRepo: (link: string, namespace: string, name: string) => void,
|
||||||
deleteRepo: (repository: Repository, () => void) => void,
|
deleteRepo: (repository: Repository, () => void) => void,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
@@ -39,9 +43,9 @@ type Props = {
|
|||||||
|
|
||||||
class RepositoryRoot extends React.Component<Props> {
|
class RepositoryRoot extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { fetchRepo, namespace, name } = this.props;
|
const { fetchRepo, namespace, name, repoLink } = this.props;
|
||||||
|
|
||||||
fetchRepo(namespace, name);
|
fetchRepo(repoLink, namespace, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
stripEndingSlash = (url: string) => {
|
stripEndingSlash = (url: string) => {
|
||||||
@@ -115,6 +119,19 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
path={`${url}/changeset/:id`}
|
path={`${url}/changeset/:id`}
|
||||||
render={() => <ChangesetView repository={repository} />}
|
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
|
<Route
|
||||||
path={`${url}/changesets`}
|
path={`${url}/changesets`}
|
||||||
render={() => (
|
render={() => (
|
||||||
@@ -141,11 +158,20 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
<Navigation>
|
<Navigation>
|
||||||
<Section label={t("repository-root.navigation-label")}>
|
<Section label={t("repository-root.navigation-label")}>
|
||||||
<NavLink to={url} label={t("repository-root.information")} />
|
<NavLink to={url} label={t("repository-root.information")} />
|
||||||
<NavLink
|
<RepositoryNavLink
|
||||||
activeOnlyWhenExact={false}
|
repository={repository}
|
||||||
|
linkName="changesets"
|
||||||
to={`${url}/changesets/`}
|
to={`${url}/changesets/`}
|
||||||
label={t("repository-root.history")}
|
label={t("repository-root.history")}
|
||||||
activeWhenMatch={this.matches}
|
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`} />
|
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||||
<PermissionsNavLink
|
<PermissionsNavLink
|
||||||
@@ -170,19 +196,21 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const repository = getRepository(state, namespace, name);
|
const repository = getRepository(state, namespace, name);
|
||||||
const loading = isFetchRepoPending(state, namespace, name);
|
const loading = isFetchRepoPending(state, namespace, name);
|
||||||
const error = getFetchRepoFailure(state, namespace, name);
|
const error = getFetchRepoFailure(state, namespace, name);
|
||||||
|
const repoLink = getRepositoriesLink(state);
|
||||||
return {
|
return {
|
||||||
namespace,
|
namespace,
|
||||||
name,
|
name,
|
||||||
repository,
|
repository,
|
||||||
loading,
|
loading,
|
||||||
error
|
error,
|
||||||
|
repoLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchRepo: (namespace: string, name: string) => {
|
fetchRepo: (link: string, namespace: string, name: string) => {
|
||||||
dispatch(fetchRepo(namespace, name));
|
dispatch(fetchRepo(link, namespace, name));
|
||||||
},
|
},
|
||||||
deleteRepo: (repository: Repository, callback: () => void) => {
|
deleteRepo: (repository: Repository, callback: () => void) => {
|
||||||
dispatch(deleteRepo(repository, callback));
|
dispatch(deleteRepo(repository, callback));
|
||||||
|
|||||||
@@ -35,20 +35,18 @@ export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
|||||||
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
|
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
|
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
const REPOS_URL = "repositories";
|
|
||||||
|
|
||||||
const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
||||||
|
|
||||||
// fetch repos
|
// fetch repos
|
||||||
|
|
||||||
const SORT_BY = "sortBy=namespaceAndName";
|
const SORT_BY = "sortBy=namespaceAndName";
|
||||||
|
|
||||||
export function fetchRepos() {
|
export function fetchRepos(link: string) {
|
||||||
return fetchReposByLink(REPOS_URL);
|
return fetchReposByLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchReposByPage(page: number) {
|
export function fetchReposByPage(link: string, page: number) {
|
||||||
return fetchReposByLink(`${REPOS_URL}?page=${page - 1}`);
|
return fetchReposByLink(`${link}?page=${page - 1}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSortByLink(url: string) {
|
function appendSortByLink(url: string) {
|
||||||
@@ -102,11 +100,12 @@ export function fetchReposFailure(err: Error): Action {
|
|||||||
|
|
||||||
// fetch repo
|
// fetch repo
|
||||||
|
|
||||||
export function fetchRepo(namespace: string, name: string) {
|
export function fetchRepo(link: string, namespace: string, name: string) {
|
||||||
|
const repoUrl = link.endsWith("/") ? link : link + "/";
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchRepoPending(namespace, name));
|
dispatch(fetchRepoPending(namespace, name));
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(`${REPOS_URL}/${namespace}/${name}`)
|
.get(`${repoUrl}${namespace}/${name}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(repository => {
|
.then(repository => {
|
||||||
dispatch(fetchRepoSuccess(repository));
|
dispatch(fetchRepoSuccess(repository));
|
||||||
@@ -154,11 +153,15 @@ export function fetchRepoFailure(
|
|||||||
|
|
||||||
// create repo
|
// create repo
|
||||||
|
|
||||||
export function createRepo(repository: Repository, callback?: () => void) {
|
export function createRepo(
|
||||||
|
link: string,
|
||||||
|
repository: Repository,
|
||||||
|
callback?: () => void
|
||||||
|
) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(createRepoPending());
|
dispatch(createRepoPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(REPOS_URL, repository, CONTENT_TYPE)
|
.post(link, repository, CONTENT_TYPE)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(createRepoSuccess());
|
dispatch(createRepoSuccess());
|
||||||
if (callback) {
|
if (callback) {
|
||||||
@@ -448,3 +451,12 @@ export function getDeleteRepoFailure(
|
|||||||
) {
|
) {
|
||||||
return getFailure(state, DELETE_REPO, namespace + "/" + name);
|
return getFailure(state, DELETE_REPO, namespace + "/" + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPermissionsLink(
|
||||||
|
state: Object,
|
||||||
|
namespace: string,
|
||||||
|
name: string
|
||||||
|
) {
|
||||||
|
const repo = getRepository(state, namespace, name);
|
||||||
|
return repo && repo._links ? repo._links.permissions.href : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ import reducer, {
|
|||||||
MODIFY_REPO,
|
MODIFY_REPO,
|
||||||
isModifyRepoPending,
|
isModifyRepoPending,
|
||||||
getModifyRepoFailure,
|
getModifyRepoFailure,
|
||||||
modifyRepoSuccess
|
modifyRepoSuccess,
|
||||||
|
getPermissionsLink
|
||||||
} from "./repos";
|
} from "./repos";
|
||||||
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
@@ -99,16 +100,13 @@ const hitchhikerRestatend: Repository = {
|
|||||||
type: "git",
|
type: "git",
|
||||||
_links: {
|
_links: {
|
||||||
self: {
|
self: {
|
||||||
href:
|
href: "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||||
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
href:
|
href: "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||||
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
href:
|
href: "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||||
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
href:
|
href:
|
||||||
@@ -158,16 +156,14 @@ const slartiFjords: Repository = {
|
|||||||
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/tags/"
|
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/tags/"
|
||||||
},
|
},
|
||||||
branches: {
|
branches: {
|
||||||
href:
|
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/branches/"
|
||||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/branches/"
|
|
||||||
},
|
},
|
||||||
changesets: {
|
changesets: {
|
||||||
href:
|
href:
|
||||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/changesets/"
|
"http://localhost:8081/api/v2/repositories/slarti/fjords/changesets/"
|
||||||
},
|
},
|
||||||
sources: {
|
sources: {
|
||||||
href:
|
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/sources/"
|
||||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/sources/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -221,6 +217,7 @@ const repositoryCollectionWithNames: RepositoryCollection = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("repos fetch", () => {
|
describe("repos fetch", () => {
|
||||||
|
const URL = "repositories";
|
||||||
const REPOS_URL = "/api/v2/repositories";
|
const REPOS_URL = "/api/v2/repositories";
|
||||||
const SORT = "sortBy=namespaceAndName";
|
const SORT = "sortBy=namespaceAndName";
|
||||||
const REPOS_URL_WITH_SORT = REPOS_URL + "?" + SORT;
|
const REPOS_URL_WITH_SORT = REPOS_URL + "?" + SORT;
|
||||||
@@ -243,7 +240,7 @@ describe("repos fetch", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchRepos()).then(() => {
|
return store.dispatch(fetchRepos(URL)).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -262,7 +259,7 @@ describe("repos fetch", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(fetchReposByPage(43)).then(() => {
|
return store.dispatch(fetchReposByPage(URL, 43)).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -318,7 +315,7 @@ describe("repos fetch", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchRepos()).then(() => {
|
return store.dispatch(fetchRepos(URL)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_REPOS_PENDING);
|
expect(actions[0].type).toEqual(FETCH_REPOS_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_REPOS_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_REPOS_FAILURE);
|
||||||
@@ -346,7 +343,7 @@ describe("repos fetch", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchRepo("slarti", "fjords")).then(() => {
|
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -357,7 +354,7 @@ describe("repos fetch", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchRepo("slarti", "fjords")).then(() => {
|
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||||
@@ -383,7 +380,7 @@ describe("repos fetch", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createRepo(slartiFjords)).then(() => {
|
return store.dispatch(createRepo(URL, slartiFjords)).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -400,7 +397,7 @@ describe("repos fetch", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createRepo(slartiFjords, callback)).then(() => {
|
return store.dispatch(createRepo(URL, slartiFjords, callback)).then(() => {
|
||||||
expect(callMe).toBe("yeah");
|
expect(callMe).toBe("yeah");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -411,7 +408,7 @@ describe("repos fetch", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createRepo(slartiFjords)).then(() => {
|
return store.dispatch(createRepo(URL, slartiFjords)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_REPO_PENDING);
|
expect(actions[0].type).toEqual(CREATE_REPO_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_REPO_FAILURE);
|
expect(actions[1].type).toEqual(CREATE_REPO_FAILURE);
|
||||||
@@ -649,6 +646,21 @@ describe("repos selectors", () => {
|
|||||||
expect(repository).toEqual(slartiFjords);
|
expect(repository).toEqual(slartiFjords);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return permissions link", () => {
|
||||||
|
const state = {
|
||||||
|
repos: {
|
||||||
|
byNames: {
|
||||||
|
"slarti/fjords": slartiFjords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = getPermissionsLink(state, "slarti", "fjords");
|
||||||
|
expect(link).toEqual(
|
||||||
|
"http://localhost:8081/api/v2/repositories/slarti/fjords/permissions/"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should return true, when fetch repo is pending", () => {
|
it("should return true, when fetch repo is pending", () => {
|
||||||
const state = {
|
const state = {
|
||||||
pending: {
|
pending: {
|
||||||
|
|||||||
@@ -48,14 +48,17 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
|||||||
onChange={this.handleNameChange}
|
onChange={this.handleNameChange}
|
||||||
validationError={!this.state.valid}
|
validationError={!this.state.valid}
|
||||||
errorMessage={t("permission.add-permission.name-input-invalid")}
|
errorMessage={t("permission.add-permission.name-input-invalid")}
|
||||||
|
helpText={t("permission.help.nameHelpText")}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t("permission.group-permission")}
|
label={t("permission.group-permission")}
|
||||||
checked={groupPermission ? groupPermission : false}
|
checked={groupPermission ? groupPermission : false}
|
||||||
onChange={this.handleGroupPermissionChange}
|
onChange={this.handleGroupPermissionChange}
|
||||||
|
helpText={t("permission.help.groupPermissionHelpText")}
|
||||||
/>
|
/>
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
label={t("permission.type")}
|
label={t("permission.type")}
|
||||||
|
helpText={t("permission.help.typeHelpText")}
|
||||||
handleTypeChange={this.handleTypeChange}
|
handleTypeChange={this.handleTypeChange}
|
||||||
type={type ? type : "READ"}
|
type={type ? type : "READ"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ type Props = {
|
|||||||
t: string => string,
|
t: string => string,
|
||||||
handleTypeChange: string => void,
|
handleTypeChange: string => void,
|
||||||
type: string,
|
type: string,
|
||||||
|
label?: string,
|
||||||
|
helpText?: string,
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class TypeSelector extends React.Component<Props> {
|
class TypeSelector extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { type, handleTypeChange, loading } = this.props;
|
const { type, handleTypeChange, loading, label, helpText } = this.props;
|
||||||
const types = ["READ", "WRITE", "OWNER"];
|
const types = ["READ", "OWNER", "WRITE"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -21,6 +23,8 @@ class TypeSelector extends React.Component<Props> {
|
|||||||
value={type ? type : "READ"}
|
value={type ? type : "READ"}
|
||||||
options={this.createSelectOptions(types)}
|
options={this.createSelectOptions(types)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
label={label}
|
||||||
|
helpText={helpText}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,4 +39,4 @@ class TypeSelector extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("permissions")(TypeSelector);
|
export default translate("repos")(TypeSelector);
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {translate} from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
createPermission,
|
|
||||||
createPermissionReset,
|
|
||||||
deletePermissionReset,
|
|
||||||
fetchPermissions,
|
fetchPermissions,
|
||||||
getCreatePermissionFailure,
|
|
||||||
getDeletePermissionsFailure,
|
|
||||||
getFetchPermissionsFailure,
|
getFetchPermissionsFailure,
|
||||||
getModifyPermissionsFailure,
|
isFetchPermissionsPending,
|
||||||
getPermissionsOfRepo,
|
getPermissionsOfRepo,
|
||||||
hasCreatePermission,
|
hasCreatePermission,
|
||||||
|
createPermission,
|
||||||
isCreatePermissionPending,
|
isCreatePermissionPending,
|
||||||
isFetchPermissionsPending,
|
getCreatePermissionFailure,
|
||||||
modifyPermissionReset
|
createPermissionReset,
|
||||||
|
getDeletePermissionsFailure,
|
||||||
|
getModifyPermissionsFailure,
|
||||||
|
modifyPermissionReset,
|
||||||
|
deletePermissionReset
|
||||||
} from "../modules/permissions";
|
} from "../modules/permissions";
|
||||||
import {ErrorPage, Loading} from "@scm-manager/ui-components";
|
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||||
import type {Permission, PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types";
|
import type {
|
||||||
|
Permission,
|
||||||
|
PermissionCollection,
|
||||||
|
PermissionCreateEntry
|
||||||
|
} from "@scm-manager/ui-types";
|
||||||
import SinglePermission from "./SinglePermission";
|
import SinglePermission from "./SinglePermission";
|
||||||
import CreatePermissionForm from "../components/CreatePermissionForm";
|
import CreatePermissionForm from "../components/CreatePermissionForm";
|
||||||
import type {History} from "history";
|
import type { History } from "history";
|
||||||
|
import { getPermissionsLink } from "../../modules/repos";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespace: string,
|
namespace: string,
|
||||||
@@ -31,10 +36,12 @@ type Props = {
|
|||||||
permissions: PermissionCollection,
|
permissions: PermissionCollection,
|
||||||
hasPermissionToCreate: boolean,
|
hasPermissionToCreate: boolean,
|
||||||
loadingCreatePermission: boolean,
|
loadingCreatePermission: boolean,
|
||||||
|
permissionsLink: string,
|
||||||
|
|
||||||
//dispatch functions
|
//dispatch functions
|
||||||
fetchPermissions: (namespace: string, repoName: string) => void,
|
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
|
||||||
createPermission: (
|
createPermission: (
|
||||||
|
link: string,
|
||||||
permission: PermissionCreateEntry,
|
permission: PermissionCreateEntry,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
repoName: string,
|
repoName: string,
|
||||||
@@ -57,17 +64,19 @@ class Permissions extends React.Component<Props> {
|
|||||||
repoName,
|
repoName,
|
||||||
modifyPermissionReset,
|
modifyPermissionReset,
|
||||||
createPermissionReset,
|
createPermissionReset,
|
||||||
deletePermissionReset
|
deletePermissionReset,
|
||||||
|
permissionsLink
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
createPermissionReset(namespace, repoName);
|
createPermissionReset(namespace, repoName);
|
||||||
modifyPermissionReset(namespace, repoName);
|
modifyPermissionReset(namespace, repoName);
|
||||||
deletePermissionReset(namespace, repoName);
|
deletePermissionReset(namespace, repoName);
|
||||||
fetchPermissions(namespace, repoName);
|
fetchPermissions(permissionsLink, namespace, repoName);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPermission = (permission: Permission) => {
|
createPermission = (permission: Permission) => {
|
||||||
this.props.createPermission(
|
this.props.createPermission(
|
||||||
|
this.props.permissionsLink,
|
||||||
permission,
|
permission,
|
||||||
this.props.namespace,
|
this.props.namespace,
|
||||||
this.props.repoName
|
this.props.repoName
|
||||||
@@ -155,6 +164,7 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
repoName
|
repoName
|
||||||
);
|
);
|
||||||
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
|
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
|
||||||
|
const permissionsLink = getPermissionsLink(state, namespace, repoName);
|
||||||
return {
|
return {
|
||||||
namespace,
|
namespace,
|
||||||
repoName,
|
repoName,
|
||||||
@@ -162,22 +172,24 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
loading,
|
loading,
|
||||||
permissions,
|
permissions,
|
||||||
hasPermissionToCreate,
|
hasPermissionToCreate,
|
||||||
loadingCreatePermission
|
loadingCreatePermission,
|
||||||
|
permissionsLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchPermissions: (namespace: string, repoName: string) => {
|
fetchPermissions: (link: string, namespace: string, repoName: string) => {
|
||||||
dispatch(fetchPermissions(namespace, repoName));
|
dispatch(fetchPermissions(link, namespace, repoName));
|
||||||
},
|
},
|
||||||
createPermission: (
|
createPermission: (
|
||||||
|
link: string,
|
||||||
permission: PermissionCreateEntry,
|
permission: PermissionCreateEntry,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
repoName: string,
|
repoName: string,
|
||||||
callback?: () => void
|
callback?: () => void
|
||||||
) => {
|
) => {
|
||||||
dispatch(createPermission(permission, namespace, repoName, callback));
|
dispatch(createPermission(link, permission, namespace, repoName, callback));
|
||||||
},
|
},
|
||||||
createPermissionReset: (namespace: string, repoName: string) => {
|
createPermissionReset: (namespace: string, repoName: string) => {
|
||||||
dispatch(createPermissionReset(namespace, repoName));
|
dispatch(createPermissionReset(namespace, repoName));
|
||||||
|
|||||||
@@ -58,17 +58,19 @@ export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
|
|||||||
types.RESET_SUFFIX
|
types.RESET_SUFFIX
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const REPOS_URL = "repositories";
|
|
||||||
const PERMISSIONS_URL = "permissions";
|
|
||||||
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
|
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
|
||||||
|
|
||||||
// fetch permissions
|
// fetch permissions
|
||||||
|
|
||||||
export function fetchPermissions(namespace: string, repoName: string) {
|
export function fetchPermissions(
|
||||||
|
link: string,
|
||||||
|
namespace: string,
|
||||||
|
repoName: string
|
||||||
|
) {
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchPermissionsPending(namespace, repoName));
|
dispatch(fetchPermissionsPending(namespace, repoName));
|
||||||
return apiClient
|
return apiClient
|
||||||
.get(`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`)
|
.get(link)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(permissions => {
|
.then(permissions => {
|
||||||
dispatch(fetchPermissionsSuccess(permissions, namespace, repoName));
|
dispatch(fetchPermissionsSuccess(permissions, namespace, repoName));
|
||||||
@@ -215,6 +217,7 @@ export function modifyPermissionReset(namespace: string, repoName: string) {
|
|||||||
|
|
||||||
// create permission
|
// create permission
|
||||||
export function createPermission(
|
export function createPermission(
|
||||||
|
link: string,
|
||||||
permission: PermissionCreateEntry,
|
permission: PermissionCreateEntry,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
repoName: string,
|
repoName: string,
|
||||||
@@ -223,11 +226,7 @@ export function createPermission(
|
|||||||
return function(dispatch: Dispatch) {
|
return function(dispatch: Dispatch) {
|
||||||
dispatch(createPermissionPending(permission, namespace, repoName));
|
dispatch(createPermissionPending(permission, namespace, repoName));
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(
|
.post(link, permission, CONTENT_TYPE)
|
||||||
`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`,
|
|
||||||
permission,
|
|
||||||
CONTENT_TYPE
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const location = response.headers.get("Location");
|
const location = response.headers.get("Location");
|
||||||
return apiClient.get(location);
|
return apiClient.get(location);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ const hitchhiker_puzzle42RepoPermissions = {
|
|||||||
|
|
||||||
describe("permission fetch", () => {
|
describe("permission fetch", () => {
|
||||||
const REPOS_URL = "/api/v2/repositories";
|
const REPOS_URL = "/api/v2/repositories";
|
||||||
|
const URL = "repositories";
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -132,7 +133,13 @@ describe("permission fetch", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store
|
return store
|
||||||
.dispatch(fetchPermissions("hitchhiker", "puzzle42"))
|
.dispatch(
|
||||||
|
fetchPermissions(
|
||||||
|
URL + "/hitchhiker/puzzle42/permissions",
|
||||||
|
"hitchhiker",
|
||||||
|
"puzzle42"
|
||||||
|
)
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
@@ -145,7 +152,13 @@ describe("permission fetch", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store
|
return store
|
||||||
.dispatch(fetchPermissions("hitchhiker", "puzzle42"))
|
.dispatch(
|
||||||
|
fetchPermissions(
|
||||||
|
URL + "/hitchhiker/puzzle42/permissions",
|
||||||
|
"hitchhiker",
|
||||||
|
"puzzle42"
|
||||||
|
)
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING);
|
expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING);
|
||||||
@@ -247,6 +260,7 @@ describe("permission fetch", () => {
|
|||||||
return store
|
return store
|
||||||
.dispatch(
|
.dispatch(
|
||||||
createPermission(
|
createPermission(
|
||||||
|
URL + "/hitchhiker/puzzle42/permissions",
|
||||||
hitchhiker_puzzle42Permission_user_eins,
|
hitchhiker_puzzle42Permission_user_eins,
|
||||||
"hitchhiker",
|
"hitchhiker",
|
||||||
"puzzle42"
|
"puzzle42"
|
||||||
@@ -268,6 +282,7 @@ describe("permission fetch", () => {
|
|||||||
return store
|
return store
|
||||||
.dispatch(
|
.dispatch(
|
||||||
createPermission(
|
createPermission(
|
||||||
|
URL + "/hitchhiker/puzzle42/permissions",
|
||||||
hitchhiker_puzzle42Permission_user_eins,
|
hitchhiker_puzzle42Permission_user_eins,
|
||||||
"hitchhiker",
|
"hitchhiker",
|
||||||
"puzzle42"
|
"puzzle42"
|
||||||
@@ -304,6 +319,7 @@ describe("permission fetch", () => {
|
|||||||
return store
|
return store
|
||||||
.dispatch(
|
.dispatch(
|
||||||
createPermission(
|
createPermission(
|
||||||
|
URL + "/hitchhiker/puzzle42/permissions",
|
||||||
hitchhiker_puzzle42Permission_user_eins,
|
hitchhiker_puzzle42Permission_user_eins,
|
||||||
"hitchhiker",
|
"hitchhiker",
|
||||||
"puzzle42",
|
"puzzle42",
|
||||||
|
|||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,13 +12,15 @@ import {
|
|||||||
} from "../modules/users";
|
} from "../modules/users";
|
||||||
import { Page } from "@scm-manager/ui-components";
|
import { Page } from "@scm-manager/ui-components";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
|
import {getUsersLink} from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
error?: Error,
|
error?: Error,
|
||||||
|
usersLink: string,
|
||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
addUser: (user: User, callback?: () => void) => void,
|
addUser: (link: string, user: User, callback?: () => void) => void,
|
||||||
resetForm: () => void,
|
resetForm: () => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
@@ -37,7 +39,7 @@ class AddUser extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createUser = (user: User) => {
|
createUser = (user: User) => {
|
||||||
this.props.addUser(user, this.userCreated);
|
this.props.addUser(this.props.usersLink, user, this.userCreated);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -61,8 +63,8 @@ class AddUser extends React.Component<Props> {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
addUser: (user: User, callback?: () => void) => {
|
addUser: (link: string, user: User, callback?: () => void) => {
|
||||||
dispatch(createUser(user, callback));
|
dispatch(createUser(link, user, callback));
|
||||||
},
|
},
|
||||||
resetForm: () => {
|
resetForm: () => {
|
||||||
dispatch(createUserReset());
|
dispatch(createUserReset());
|
||||||
@@ -73,7 +75,9 @@ const mapDispatchToProps = dispatch => {
|
|||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const loading = isCreateUserPending(state);
|
const loading = isCreateUserPending(state);
|
||||||
const error = getCreateUserFailure(state);
|
const error = getCreateUserFailure(state);
|
||||||
|
const usersLink = getUsersLink(state);
|
||||||
return {
|
return {
|
||||||
|
usersLink,
|
||||||
loading,
|
loading,
|
||||||
error
|
error
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ import {
|
|||||||
|
|
||||||
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
|
import { getUsersLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
user: User,
|
user: User,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
usersLink: string,
|
||||||
|
|
||||||
// dispatcher functions
|
// dispatcher functions
|
||||||
deleteUser: (user: User, callback?: () => void) => void,
|
deleteUser: (user: User, callback?: () => void) => void,
|
||||||
fetchUser: string => void,
|
fetchUser: (string, string) => void,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -45,7 +47,7 @@ type Props = {
|
|||||||
|
|
||||||
class SingleUser extends React.Component<Props> {
|
class SingleUser extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUser(this.props.name);
|
this.props.fetchUser(this.props.usersLink, this.props.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
userDeleted = () => {
|
userDeleted = () => {
|
||||||
@@ -124,8 +126,9 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
isFetchUserPending(state, name) || isDeleteUserPending(state, name);
|
isFetchUserPending(state, name) || isDeleteUserPending(state, name);
|
||||||
const error =
|
const error =
|
||||||
getFetchUserFailure(state, name) || getDeleteUserFailure(state, name);
|
getFetchUserFailure(state, name) || getDeleteUserFailure(state, name);
|
||||||
|
const usersLink = getUsersLink(state);
|
||||||
return {
|
return {
|
||||||
|
usersLink,
|
||||||
name,
|
name,
|
||||||
user,
|
user,
|
||||||
loading,
|
loading,
|
||||||
@@ -135,8 +138,8 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchUser: (name: string) => {
|
fetchUser: (link: string, name: string) => {
|
||||||
dispatch(fetchUser(name));
|
dispatch(fetchUser(link, name));
|
||||||
},
|
},
|
||||||
deleteUser: (user: User, callback?: () => void) => {
|
deleteUser: (user: User, callback?: () => void) => {
|
||||||
dispatch(deleteUser(user, callback));
|
dispatch(deleteUser(user, callback));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Page, Paginator } from "@scm-manager/ui-components";
|
|||||||
import { UserTable } from "./../components/table";
|
import { UserTable } from "./../components/table";
|
||||||
import type { User, PagedCollection } from "@scm-manager/ui-types";
|
import type { User, PagedCollection } from "@scm-manager/ui-types";
|
||||||
import CreateUserButton from "../components/buttons/CreateUserButton";
|
import CreateUserButton from "../components/buttons/CreateUserButton";
|
||||||
|
import { getUsersLink } from "../../modules/indexResource";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
users: User[],
|
users: User[],
|
||||||
@@ -26,19 +27,20 @@ type Props = {
|
|||||||
canAddUsers: boolean,
|
canAddUsers: boolean,
|
||||||
list: PagedCollection,
|
list: PagedCollection,
|
||||||
page: number,
|
page: number,
|
||||||
|
usersLink: string,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: string => string,
|
||||||
history: History,
|
history: History,
|
||||||
|
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
fetchUsersByPage: (page: number) => void,
|
fetchUsersByPage: (link: string, page: number) => void,
|
||||||
fetchUsersByLink: (link: string) => void
|
fetchUsersByLink: (link: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
class Users extends React.Component<Props> {
|
class Users extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUsersByPage(this.props.page);
|
this.props.fetchUsersByPage(this.props.usersLink, this.props.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange = (link: string) => {
|
onPageChange = (link: string) => {
|
||||||
@@ -107,6 +109,8 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const loading = isFetchUsersPending(state);
|
const loading = isFetchUsersPending(state);
|
||||||
const error = getFetchUsersFailure(state);
|
const error = getFetchUsersFailure(state);
|
||||||
|
|
||||||
|
const usersLink = getUsersLink(state);
|
||||||
|
|
||||||
const page = getPageFromProps(ownProps);
|
const page = getPageFromProps(ownProps);
|
||||||
const canAddUsers = isPermittedToCreateUsers(state);
|
const canAddUsers = isPermittedToCreateUsers(state);
|
||||||
const list = selectListAsCollection(state);
|
const list = selectListAsCollection(state);
|
||||||
@@ -117,14 +121,15 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
error,
|
error,
|
||||||
canAddUsers,
|
canAddUsers,
|
||||||
list,
|
list,
|
||||||
page
|
page,
|
||||||
|
usersLink
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchUsersByPage: (page: number) => {
|
fetchUsersByPage: (link: string, page: number) => {
|
||||||
dispatch(fetchUsersByPage(page));
|
dispatch(fetchUsersByPage(link, page));
|
||||||
},
|
},
|
||||||
fetchUsersByLink: (link: string) => {
|
fetchUsersByLink: (link: string) => {
|
||||||
dispatch(fetchUsersByLink(link));
|
dispatch(fetchUsersByLink(link));
|
||||||
|
|||||||
@@ -32,21 +32,19 @@ export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
|
|||||||
export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
|
export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
|
||||||
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
|
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
const USERS_URL = "users";
|
|
||||||
|
|
||||||
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
||||||
|
|
||||||
// TODO i18n for error messages
|
// TODO i18n for error messages
|
||||||
|
|
||||||
// fetch users
|
// fetch users
|
||||||
|
|
||||||
export function fetchUsers() {
|
export function fetchUsers(link: string) {
|
||||||
return fetchUsersByLink(USERS_URL);
|
return fetchUsersByLink(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchUsersByPage(page: number) {
|
export function fetchUsersByPage(link: string, page: number) {
|
||||||
// backend start counting by 0
|
// backend start counting by 0
|
||||||
return fetchUsersByLink(USERS_URL + "?page=" + (page - 1));
|
return fetchUsersByLink(link + "?page=" + (page - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchUsersByLink(link: string) {
|
export function fetchUsersByLink(link: string) {
|
||||||
@@ -60,7 +58,7 @@ export function fetchUsersByLink(link: string) {
|
|||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
const error = new Error(`could not fetch users: ${cause.message}`);
|
const error = new Error(`could not fetch users: ${cause.message}`);
|
||||||
dispatch(fetchUsersFailure(USERS_URL, error));
|
dispatch(fetchUsersFailure(link, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -89,8 +87,8 @@ export function fetchUsersFailure(url: string, error: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//fetch user
|
//fetch user
|
||||||
export function fetchUser(name: string) {
|
export function fetchUser(link: string, name: string) {
|
||||||
const userUrl = USERS_URL + "/" + name;
|
const userUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(fetchUserPending(name));
|
dispatch(fetchUserPending(name));
|
||||||
return apiClient
|
return apiClient
|
||||||
@@ -137,11 +135,11 @@ export function fetchUserFailure(name: string, error: Error): Action {
|
|||||||
|
|
||||||
//create user
|
//create user
|
||||||
|
|
||||||
export function createUser(user: User, callback?: () => void) {
|
export function createUser(link: string, user: User, callback?: () => void) {
|
||||||
return function(dispatch: Dispatch) {
|
return function(dispatch: Dispatch) {
|
||||||
dispatch(createUserPending(user));
|
dispatch(createUserPending(user));
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(USERS_URL, user, CONTENT_TYPE_USER)
|
.post(link, user, CONTENT_TYPE_USER)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(createUserSuccess());
|
dispatch(createUserSuccess());
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const response = {
|
|||||||
responseBody
|
responseBody
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const URL = "users";
|
||||||
const USERS_URL = "/api/v2/users";
|
const USERS_URL = "/api/v2/users";
|
||||||
|
|
||||||
const error = new Error("KAPUTT");
|
const error = new Error("KAPUTT");
|
||||||
@@ -146,7 +147,7 @@ describe("users fetch()", () => {
|
|||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
return store.dispatch(fetchUsers()).then(() => {
|
return store.dispatch(fetchUsers(URL)).then(() => {
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -157,7 +158,7 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchUsers()).then(() => {
|
return store.dispatch(fetchUsers(URL)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_USERS_PENDING);
|
expect(actions[0].type).toEqual(FETCH_USERS_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_USERS_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_USERS_FAILURE);
|
||||||
@@ -169,7 +170,7 @@ describe("users fetch()", () => {
|
|||||||
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
|
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchUser("zaphod")).then(() => {
|
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
||||||
@@ -183,7 +184,7 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(fetchUser("zaphod")).then(() => {
|
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
||||||
@@ -201,7 +202,7 @@ describe("users fetch()", () => {
|
|||||||
fetchMock.getOnce(USERS_URL, response);
|
fetchMock.getOnce(USERS_URL, response);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createUser(userZaphod)).then(() => {
|
return store.dispatch(createUser(URL, userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
|
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
|
||||||
@@ -214,7 +215,7 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createUser(userZaphod)).then(() => {
|
return store.dispatch(createUser(URL, userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_USER_FAILURE);
|
expect(actions[1].type).toEqual(CREATE_USER_FAILURE);
|
||||||
@@ -235,7 +236,7 @@ describe("users fetch()", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createUser(userZaphod, callback)).then(() => {
|
return store.dispatch(createUser(URL, userZaphod, callback)).then(() => {
|
||||||
expect(callMe).toBe("yeah");
|
expect(callMe).toBe("yeah");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.net.URL;
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@Priority(WebResourceServlet.PRIORITY)
|
||||||
@WebElement(value = WebResourceServlet.PATTERN, regex = true)
|
@WebElement(value = WebResourceServlet.PATTERN, regex = true)
|
||||||
public class WebResourceServlet extends HttpServlet {
|
public class WebResourceServlet extends HttpServlet {
|
||||||
|
|
||||||
@@ -35,6 +36,9 @@ public class WebResourceServlet extends HttpServlet {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/|repo/).*";
|
static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/|repo/).*";
|
||||||
|
|
||||||
|
// Be sure that this servlet is the last one in the servlet chain.
|
||||||
|
static final int PRIORITY = Integer.MAX_VALUE;
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class);
|
private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class);
|
||||||
|
|
||||||
private final WebResourceSender sender = WebResourceSender.create()
|
private final WebResourceSender sender = WebResourceSender.create()
|
||||||
|
|||||||
@@ -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;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import de.otto.edison.hal.HalRepresentation;
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
import de.otto.edison.hal.Links;
|
import de.otto.edison.hal.Links;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -7,6 +8,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -15,14 +17,26 @@ public class FileObjectDto extends HalRepresentation {
|
|||||||
private String name;
|
private String name;
|
||||||
private String path;
|
private String path;
|
||||||
private boolean directory;
|
private boolean directory;
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
private String description;
|
private String description;
|
||||||
private int length;
|
private long length;
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
private Instant lastModified;
|
private Instant lastModified;
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
private SubRepositoryDto subRepository;
|
private SubRepositoryDto subRepository;
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
private String revision;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||||
protected HalRepresentation add(Links links) {
|
protected HalRepresentation add(Links links) {
|
||||||
return super.add(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 javax.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static de.otto.edison.hal.Link.link;
|
import static de.otto.edison.hal.Link.link;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sonia.scm.repository.NamespaceAndName;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
class ResourceLinks {
|
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'
|
// 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) {
|
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() {
|
GroupLinks group() {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package sonia.scm.api.v2.resources;
|
|||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
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.BrowseCommandBuilder;
|
||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
@@ -21,26 +19,26 @@ import java.io.IOException;
|
|||||||
public class SourceRootResource {
|
public class SourceRootResource {
|
||||||
|
|
||||||
private final RepositoryServiceFactory serviceFactory;
|
private final RepositoryServiceFactory serviceFactory;
|
||||||
private final BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper;
|
private final BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper;
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SourceRootResource(RepositoryServiceFactory serviceFactory, BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper) {
|
public SourceRootResource(RepositoryServiceFactory serviceFactory, BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper) {
|
||||||
this.serviceFactory = serviceFactory;
|
this.serviceFactory = serviceFactory;
|
||||||
this.browserResultToBrowserResultDtoMapper = browserResultToBrowserResultDtoMapper;
|
this.browserResultToFileObjectDtoMapper = browserResultToFileObjectDtoMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(VndMediaType.SOURCE)
|
@Produces(VndMediaType.SOURCE)
|
||||||
@Path("")
|
@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);
|
return getSource(namespace, name, "/", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(VndMediaType.SOURCE)
|
@Produces(VndMediaType.SOURCE)
|
||||||
@Path("{revision}")
|
@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);
|
return getSource(namespace, name, "/", revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +49,7 @@ public class SourceRootResource {
|
|||||||
return getSource(namespace, name, path, revision);
|
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);
|
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, repoName);
|
||||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||||
BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand();
|
BrowseCommandBuilder browseCommand = repositoryService.getBrowseCommand();
|
||||||
@@ -59,10 +57,11 @@ public class SourceRootResource {
|
|||||||
if (revision != null && !revision.isEmpty()) {
|
if (revision != null && !revision.isEmpty()) {
|
||||||
browseCommand.setRevision(revision);
|
browseCommand.setRevision(revision);
|
||||||
}
|
}
|
||||||
|
browseCommand.setDisableCache(true);
|
||||||
BrowserResult browserResult = browseCommand.getBrowserResult();
|
BrowserResult browserResult = browseCommand.getBrowserResult();
|
||||||
|
|
||||||
if (browserResult != null) {
|
if (browserResult != null) {
|
||||||
return Response.ok(browserResultToBrowserResultDtoMapper.map(browserResult, namespaceAndName, path)).build();
|
return Response.ok(browserResultToFileObjectDtoMapper.map(browserResult, namespaceAndName)).build();
|
||||||
} else {
|
} else {
|
||||||
return Response.status(Response.Status.NOT_FOUND).build();
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
|
|||||||
188
scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java
Normal file
188
scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package sonia.scm.web.i18n;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.github.legman.Subscribe;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
|
import sonia.scm.SCMContext;
|
||||||
|
import sonia.scm.Stage;
|
||||||
|
import sonia.scm.boot.RestartEvent;
|
||||||
|
import sonia.scm.cache.Cache;
|
||||||
|
import sonia.scm.cache.CacheManager;
|
||||||
|
import sonia.scm.filter.WebElement;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect the plugin translations.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@WebElement(value = I18nServlet.PATTERN, regex = true)
|
||||||
|
@Slf4j
|
||||||
|
public class I18nServlet extends HttpServlet {
|
||||||
|
|
||||||
|
public static final String PLUGINS_JSON = "plugins.json";
|
||||||
|
public static final String PATTERN = "/locales/[a-z\\-A-Z]*/" + PLUGINS_JSON;
|
||||||
|
public static final String CACHE_NAME = "sonia.cache.plugins.translations";
|
||||||
|
|
||||||
|
private final ClassLoader classLoader;
|
||||||
|
private final Cache<String, JsonNode> cache;
|
||||||
|
private static ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public I18nServlet(PluginLoader pluginLoader, CacheManager cacheManager) {
|
||||||
|
this.classLoader = pluginLoader.getUberClassLoader();
|
||||||
|
this.cache = cacheManager.getCache(CACHE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(async = false)
|
||||||
|
public void handleRestartEvent(RestartEvent event) {
|
||||||
|
log.debug("Clear cache on restart event with reason {}", event.getReason());
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode getCollectedJson(String path,
|
||||||
|
Function<String, Optional<JsonNode>> jsonFileProvider,
|
||||||
|
BiConsumer<String, JsonNode> createdJsonFileConsumer) {
|
||||||
|
return Optional.ofNullable(jsonFileProvider.apply(path)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
Optional<JsonNode> createdFile = collectJsonFile(path);
|
||||||
|
createdFile.ifPresent(map -> createdJsonFileConsumer.accept(path, map));
|
||||||
|
return createdFile.orElse(null);
|
||||||
|
}
|
||||||
|
)).orElseThrow(NotFoundException::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse response) {
|
||||||
|
try (PrintWriter out = response.getWriter()) {
|
||||||
|
response.setContentType("application/json");
|
||||||
|
String path = req.getServletPath();
|
||||||
|
Function<String, Optional<JsonNode>> jsonFileProvider = usedPath -> Optional.empty();
|
||||||
|
BiConsumer<String, JsonNode> createdJsonFileConsumer = (usedPath, jsonNode) -> log.debug("A json File is created from the path {}", usedPath);
|
||||||
|
if (isProductionStage()) {
|
||||||
|
log.debug("In Production Stage get the plugin translations from the cache");
|
||||||
|
jsonFileProvider = usedPath -> Optional.ofNullable(
|
||||||
|
cache.get(usedPath));
|
||||||
|
createdJsonFileConsumer = createdJsonFileConsumer
|
||||||
|
.andThen((usedPath, jsonNode) -> log.debug("Put the created json File in the cache with the key {}", usedPath))
|
||||||
|
.andThen(cache::put);
|
||||||
|
}
|
||||||
|
objectMapper.writeValue(out, getCollectedJson(path, jsonFileProvider, createdJsonFileConsumer));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error on getting the translation of the plugins", e);
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.error("Plugin translations are not found", e);
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected boolean isProductionStage() {
|
||||||
|
return SCMContext.getContext().getStage() == Stage.PRODUCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a collected Json File as JsonNode from the given path from all plugins in the class path
|
||||||
|
*
|
||||||
|
* @param path the searched resource path
|
||||||
|
* @return a collected Json File as JsonNode from the given path from all plugins in the class path
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
protected Optional<JsonNode> collectJsonFile(String path) {
|
||||||
|
log.debug("Collect plugin translations from path {} for every plugin", path);
|
||||||
|
JsonNode mergedJsonNode = null;
|
||||||
|
try {
|
||||||
|
Enumeration<URL> resources = classLoader.getResources(path.replaceFirst("/", ""));
|
||||||
|
while (resources.hasMoreElements()) {
|
||||||
|
URL url = resources.nextElement();
|
||||||
|
JsonNode jsonNode = objectMapper.readTree(url);
|
||||||
|
if (mergedJsonNode != null) {
|
||||||
|
merge(mergedJsonNode, jsonNode);
|
||||||
|
} else {
|
||||||
|
mergedJsonNode = jsonNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error on loading sources from {}", path, e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(mergedJsonNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge the <code>updateNode</code> into the <code>mainNode</code> and return it.
|
||||||
|
*
|
||||||
|
* This is not a deep merge.
|
||||||
|
*
|
||||||
|
* @param mainNode the main node
|
||||||
|
* @param updateNode the update node
|
||||||
|
* @return the merged mainNode
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
protected JsonNode merge(JsonNode mainNode, JsonNode updateNode) {
|
||||||
|
Iterator<String> fieldNames = updateNode.fieldNames();
|
||||||
|
|
||||||
|
while (fieldNames.hasNext()) {
|
||||||
|
|
||||||
|
String fieldName = fieldNames.next();
|
||||||
|
JsonNode jsonNode = mainNode.get(fieldName);
|
||||||
|
|
||||||
|
if (jsonNode != null) {
|
||||||
|
mergeNode(updateNode, fieldName, jsonNode);
|
||||||
|
} else {
|
||||||
|
mergeField(mainNode, updateNode, fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mainNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeField(JsonNode mainNode, JsonNode updateNode, String fieldName) {
|
||||||
|
if (mainNode instanceof ObjectNode) {
|
||||||
|
JsonNode value = updateNode.get(fieldName);
|
||||||
|
if (value.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.isIntegralNumber() && value.toString().equals("0")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.isFloatingPointNumber() && value.toString().equals("0.0")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
((ObjectNode) mainNode).set(fieldName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeNode(JsonNode updateNode, String fieldName, JsonNode jsonNode) {
|
||||||
|
if (jsonNode.isObject()) {
|
||||||
|
merge(jsonNode, updateNode.get(fieldName));
|
||||||
|
} else if (jsonNode.isArray()) {
|
||||||
|
for (int i = 0; i < jsonNode.size(); i++) {
|
||||||
|
merge(jsonNode.get(i), updateNode.get(fieldName).get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,34 +8,26 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.mockito.MockitoAnnotations.initMocks;
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
|
|
||||||
public class BrowserResultToBrowserResultDtoMapperTest {
|
public class BrowserResultToFileObjectDtoMapperTest {
|
||||||
|
|
||||||
private final URI baseUri = URI.create("http://example.com/base/");
|
private final URI baseUri = URI.create("http://example.com/base/");
|
||||||
@SuppressWarnings("unused") // Is injected
|
@SuppressWarnings("unused") // Is injected
|
||||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||||
|
|
||||||
@Mock
|
|
||||||
private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private BrowserResultToBrowserResultDtoMapper mapper;
|
private FileObjectToFileObjectDtoMapperImpl fileObjectToFileObjectDtoMapper;
|
||||||
|
|
||||||
|
private BrowserResultToFileObjectDtoMapper mapper;
|
||||||
|
|
||||||
private final Subject subject = mock(Subject.class);
|
private final Subject subject = mock(Subject.class);
|
||||||
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||||
@@ -47,6 +39,7 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
|||||||
@Before
|
@Before
|
||||||
public void init() {
|
public void init() {
|
||||||
initMocks(this);
|
initMocks(this);
|
||||||
|
mapper = new BrowserResultToFileObjectDtoMapper(fileObjectToFileObjectDtoMapper);
|
||||||
subjectThreadState.bind();
|
subjectThreadState.bind();
|
||||||
ThreadContext.bind(subject);
|
ThreadContext.bind(subject);
|
||||||
|
|
||||||
@@ -63,9 +56,6 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
|||||||
fileObject2.setPath("/path/object/2");
|
fileObject2.setPath("/path/object/2");
|
||||||
fileObject2.setDescription("description of file object 2");
|
fileObject2.setDescription("description of file object 2");
|
||||||
fileObject2.setDirectory(true);
|
fileObject2.setDirectory(true);
|
||||||
|
|
||||||
when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString()))
|
|
||||||
.thenReturn(new FileObjectDto());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -77,7 +67,7 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
|||||||
public void shouldMapAttributesCorrectly() {
|
public void shouldMapAttributesCorrectly() {
|
||||||
BrowserResult browserResult = createBrowserResult();
|
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);
|
assertEqualAttributes(browserResult, dto);
|
||||||
}
|
}
|
||||||
@@ -87,10 +77,9 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
|||||||
BrowserResult browserResult = createBrowserResult();
|
BrowserResult browserResult = createBrowserResult();
|
||||||
NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar");
|
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");
|
assertThat(dto.getEmbedded().getItemsBy("children")).hasSize(2);
|
||||||
verify(fileObjectToFileObjectDtoMapper).map(fileObject2, namespaceAndName, "Revision");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -98,28 +87,27 @@ public class BrowserResultToBrowserResultDtoMapperTest {
|
|||||||
BrowserResult browserResult = createBrowserResult();
|
BrowserResult browserResult = createBrowserResult();
|
||||||
NamespaceAndName namespaceAndName = new NamespaceAndName("foo", "bar");
|
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");
|
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).contains("path");
|
||||||
}
|
}
|
||||||
|
|
||||||
private BrowserResult createBrowserResult() {
|
private BrowserResult createBrowserResult() {
|
||||||
BrowserResult browserResult = new BrowserResult();
|
return new BrowserResult("Revision", createFileObject());
|
||||||
browserResult.setRevision("Revision");
|
|
||||||
browserResult.setFiles(createFileObjects());
|
|
||||||
|
|
||||||
return browserResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FileObject> createFileObjects() {
|
private FileObject createFileObject() {
|
||||||
List<FileObject> fileObjects = new ArrayList<>();
|
FileObject file = new FileObject();
|
||||||
|
file.setName("");
|
||||||
|
file.setPath("/path");
|
||||||
|
file.setDirectory(true);
|
||||||
|
|
||||||
fileObjects.add(fileObject1);
|
file.addChild(fileObject1);
|
||||||
fileObjects.add(fileObject2);
|
file.addChild(fileObject2);
|
||||||
return fileObjects;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertEqualAttributes(BrowserResult browserResult, BrowserResultDto dto) {
|
private void assertEqualAttributes(BrowserResult browserResult, FileObjectDto dto) {
|
||||||
assertThat(dto.getRevision()).isEqualTo(browserResult.getRevision());
|
assertThat(dto.getRevision()).isEqualTo(browserResult.getRevision());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +173,30 @@ public class ResourceLinksTest {
|
|||||||
assertEquals(BASE_URL + ConfigResource.CONFIG_PATH_V2, url);
|
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
|
@Before
|
||||||
public void initUriInfo() {
|
public void initUriInfo() {
|
||||||
initMocks(this);
|
initMocks(this);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.BrowserResult;
|
import sonia.scm.repository.BrowserResult;
|
||||||
import sonia.scm.repository.FileObject;
|
import sonia.scm.repository.FileObject;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
import sonia.scm.repository.RepositoryNotFoundException;
|
import sonia.scm.repository.RepositoryNotFoundException;
|
||||||
import sonia.scm.repository.RevisionNotFoundException;
|
|
||||||
import sonia.scm.repository.api.BrowseCommandBuilder;
|
import sonia.scm.repository.api.BrowseCommandBuilder;
|
||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
@@ -22,12 +22,8 @@ import sonia.scm.repository.api.RepositoryServiceFactory;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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 org.mockito.Mockito.when;
|
||||||
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
|
||||||
|
|
||||||
@@ -46,30 +42,25 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
|||||||
@Mock
|
@Mock
|
||||||
private BrowseCommandBuilder browseCommandBuilder;
|
private BrowseCommandBuilder browseCommandBuilder;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private FileObjectToFileObjectDtoMapper fileObjectToFileObjectDtoMapper;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private BrowserResultToBrowserResultDtoMapper browserResultToBrowserResultDtoMapper;
|
private FileObjectToFileObjectDtoMapperImpl fileObjectToFileObjectDtoMapper;
|
||||||
|
|
||||||
|
private BrowserResultToFileObjectDtoMapper browserResultToFileObjectDtoMapper;
|
||||||
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void prepareEnvironment() throws Exception {
|
public void prepareEnvironment() throws Exception {
|
||||||
|
browserResultToFileObjectDtoMapper = new BrowserResultToFileObjectDtoMapper(fileObjectToFileObjectDtoMapper);
|
||||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||||
when(service.getBrowseCommand()).thenReturn(browseCommandBuilder);
|
when(service.getBrowseCommand()).thenReturn(browseCommandBuilder);
|
||||||
|
|
||||||
FileObjectDto dto = new FileObjectDto();
|
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToFileObjectDtoMapper);
|
||||||
dto.setName("name");
|
|
||||||
dto.setLength(1024);
|
|
||||||
|
|
||||||
when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto);
|
|
||||||
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper);
|
|
||||||
super.sourceRootResource = Providers.of(sourceRootResource);
|
super.sourceRootResource = Providers.of(sourceRootResource);
|
||||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldReturnSources() throws URISyntaxException, IOException, RevisionNotFoundException {
|
public void shouldReturnSources() throws URISyntaxException, IOException, NotFoundException {
|
||||||
BrowserResult result = createBrowserResult();
|
BrowserResult result = createBrowserResult();
|
||||||
when(browseCommandBuilder.getBrowserResult()).thenReturn(result);
|
when(browseCommandBuilder.getBrowserResult()).thenReturn(result);
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources");
|
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources");
|
||||||
@@ -77,8 +68,9 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
System.out.println(response.getContentAsString());
|
||||||
assertThat(response.getContentAsString()).contains("\"revision\":\"revision\"");
|
assertThat(response.getContentAsString()).contains("\"revision\":\"revision\"");
|
||||||
assertThat(response.getContentAsString()).contains("\"files\":");
|
assertThat(response.getContentAsString()).contains("\"children\":");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -92,13 +84,11 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, RevisionNotFoundException {
|
public void shouldGetResultForSingleFile() throws URISyntaxException, IOException, NotFoundException {
|
||||||
BrowserResult browserResult = new BrowserResult();
|
|
||||||
browserResult.setRevision("revision");
|
|
||||||
FileObject fileObject = new FileObject();
|
FileObject fileObject = new FileObject();
|
||||||
fileObject.setName("File Object!");
|
fileObject.setName("File Object!");
|
||||||
|
fileObject.setPath("/");
|
||||||
browserResult.setFiles(Arrays.asList(fileObject));
|
BrowserResult browserResult = new BrowserResult("revision", fileObject);
|
||||||
|
|
||||||
when(browseCommandBuilder.getBrowserResult()).thenReturn(browserResult);
|
when(browseCommandBuilder.getBrowserResult()).thenReturn(browserResult);
|
||||||
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/revision/fileabc");
|
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() {
|
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();
|
FileObject fileObject1 = new FileObject();
|
||||||
fileObject1.setName("FO 1");
|
fileObject1.setName("FO 1");
|
||||||
fileObject1.setDirectory(false);
|
fileObject1.setDirectory(false);
|
||||||
@@ -132,6 +127,7 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
|||||||
fileObject1.setPath("/foo/bar/fo1");
|
fileObject1.setPath("/foo/bar/fo1");
|
||||||
fileObject1.setLength(1024L);
|
fileObject1.setLength(1024L);
|
||||||
fileObject1.setLastModified(0L);
|
fileObject1.setLastModified(0L);
|
||||||
|
parent.addChild(fileObject1);
|
||||||
|
|
||||||
FileObject fileObject2 = new FileObject();
|
FileObject fileObject2 = new FileObject();
|
||||||
fileObject2.setName("FO 2");
|
fileObject2.setName("FO 2");
|
||||||
@@ -140,7 +136,8 @@ public class SourceRootResourceTest extends RepositoryTestBase {
|
|||||||
fileObject2.setPath("/foo/bar/fo2");
|
fileObject2.setPath("/foo/bar/fo2");
|
||||||
fileObject2.setLength(4096L);
|
fileObject2.setLength(4096L);
|
||||||
fileObject2.setLastModified(1234L);
|
fileObject2.setLastModified(1234L);
|
||||||
|
parent.addChild(fileObject2);
|
||||||
|
|
||||||
return Arrays.asList(fileObject1, fileObject2);
|
return parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
255
scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java
Normal file
255
scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
package sonia.scm.web.i18n;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.io.Files;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.assertj.core.util.Lists;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockSettings;
|
||||||
|
import org.mockito.internal.creation.MockSettingsImpl;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import sonia.scm.boot.RestartEvent;
|
||||||
|
import sonia.scm.cache.Cache;
|
||||||
|
import sonia.scm.cache.CacheManager;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||||
|
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
|
||||||
|
public class I18nServletTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
private static final String GIT_PLUGIN_JSON = json(
|
||||||
|
"{",
|
||||||
|
"'scm-git-plugin': {",
|
||||||
|
"'information': {",
|
||||||
|
"'clone' : 'Clone',",
|
||||||
|
"'create' : 'Create',",
|
||||||
|
"'replace' : 'Push'",
|
||||||
|
"}",
|
||||||
|
"}",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
private static final String HG_PLUGIN_JSON = json(
|
||||||
|
"{",
|
||||||
|
"'scm-hg-plugin': {",
|
||||||
|
"'information': {",
|
||||||
|
"'clone' : 'Clone',",
|
||||||
|
"'create' : 'Create',",
|
||||||
|
"'replace' : 'Push'",
|
||||||
|
"}",
|
||||||
|
"}",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
private static String SVN_PLUGIN_JSON = json(
|
||||||
|
"{",
|
||||||
|
"'scm-svn-plugin': {",
|
||||||
|
"'information': {",
|
||||||
|
"'checkout' : 'Checkout'",
|
||||||
|
"}",
|
||||||
|
"}",
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static String json(String... parts) {
|
||||||
|
return String.join("\n", parts ).replaceAll("'", "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
PluginLoader pluginLoader;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
CacheManager cacheManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ClassLoader classLoader;
|
||||||
|
|
||||||
|
I18nServlet servlet;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Cache cache;
|
||||||
|
private Enumeration<URL> resources;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void init() throws IOException {
|
||||||
|
resources = Collections.enumeration(Lists.newArrayList(
|
||||||
|
createFileFromString(SVN_PLUGIN_JSON).toURL(),
|
||||||
|
createFileFromString(GIT_PLUGIN_JSON).toURL(),
|
||||||
|
createFileFromString(HG_PLUGIN_JSON).toURL()
|
||||||
|
));
|
||||||
|
when(pluginLoader.getUberClassLoader()).thenReturn(classLoader);
|
||||||
|
when(cacheManager.getCache(I18nServlet.CACHE_NAME)).thenReturn(cache);
|
||||||
|
MockSettings settings = new MockSettingsImpl<>();
|
||||||
|
settings.useConstructor(pluginLoader, cacheManager);
|
||||||
|
settings.defaultAnswer(InvocationOnMock::callRealMethod);
|
||||||
|
servlet = mock(I18nServlet.class, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCleanCacheOnRestartEvent() {
|
||||||
|
ScmEventBus.getInstance().register(servlet);
|
||||||
|
|
||||||
|
ScmEventBus.getInstance().post(new RestartEvent(I18nServlet.class, "Restart to reload the plugin resources"));
|
||||||
|
|
||||||
|
verify(cache).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldFailWith404OnMissingResources() throws IOException {
|
||||||
|
String path = "/locales/de/plugins.json";
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
PrintWriter writer = mock(PrintWriter.class);
|
||||||
|
when(response.getWriter()).thenReturn(writer);
|
||||||
|
when(request.getServletPath()).thenReturn(path);
|
||||||
|
when(classLoader.getResources("locales/de/plugins.json")).thenThrow(IOException.class);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldFailWith500OnIOException() throws IOException {
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
doThrow(IOException.class).when(response).getWriter();
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
verify(response).setStatus(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void inDevelopmentStageShouldNotUseCache() throws IOException {
|
||||||
|
String path = "/locales/de/plugins.json";
|
||||||
|
when(servlet.isProductionStage()).thenReturn(false);
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
File file = temporaryFolder.newFile();
|
||||||
|
PrintWriter writer = new PrintWriter(new FileOutputStream(file));
|
||||||
|
when(response.getWriter()).thenReturn(writer);
|
||||||
|
when(request.getServletPath()).thenReturn(path);
|
||||||
|
when(classLoader.getResources("locales/de/plugins.json")).thenReturn(resources);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
String json = Files.readLines(file, Charset.defaultCharset()).get(0);
|
||||||
|
assertJson(json);
|
||||||
|
verify(cache, never()).get(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void inProductionStageShouldUseCache() throws IOException {
|
||||||
|
String path = "/locales/de/plugins.json";
|
||||||
|
when(servlet.isProductionStage()).thenReturn(true);
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
File file = temporaryFolder.newFile();
|
||||||
|
PrintWriter writer = new PrintWriter(new FileOutputStream(file));
|
||||||
|
when(response.getWriter()).thenReturn(writer);
|
||||||
|
when(request.getServletPath()).thenReturn(path);
|
||||||
|
when(classLoader.getResources("locales/de/plugins.json")).thenReturn(resources);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
String json = Files.readLines(file, Charset.defaultCharset()).get(0);
|
||||||
|
assertJson(json);
|
||||||
|
verify(cache).get(path);
|
||||||
|
verify(cache).put(eq(path), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void inProductionStageShouldGetFromCache() throws IOException {
|
||||||
|
String path = "/locales/de/plugins.json";
|
||||||
|
when(servlet.isProductionStage()).thenReturn(true);
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
File file = temporaryFolder.newFile();
|
||||||
|
PrintWriter writer = new PrintWriter(new FileOutputStream(file));
|
||||||
|
when(response.getWriter()).thenReturn(writer);
|
||||||
|
when(request.getServletPath()).thenReturn(path);
|
||||||
|
when(classLoader.getResources("locales/de/plugins.json")).thenReturn(resources);
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
JsonNode node = objectMapper.readTree(GIT_PLUGIN_JSON);
|
||||||
|
node = servlet.merge(node, objectMapper.readTree(HG_PLUGIN_JSON));
|
||||||
|
node = servlet.merge(node, objectMapper.readTree(SVN_PLUGIN_JSON));
|
||||||
|
when(cache.get(path)).thenReturn(node);
|
||||||
|
|
||||||
|
servlet.doGet(request, response);
|
||||||
|
|
||||||
|
String json = Files.readLines(file, Charset.defaultCharset()).get(0);
|
||||||
|
verify(servlet, never()).collectJsonFile(path);
|
||||||
|
verify(cache, never()).put(eq(path), any());
|
||||||
|
verify(cache).get(path);
|
||||||
|
assertJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldCollectJsonFile() throws IOException {
|
||||||
|
String path = "locales/de/plugins.json";
|
||||||
|
when(classLoader.getResources(path)).thenReturn(resources);
|
||||||
|
|
||||||
|
Optional<JsonNode> jsonNodeOptional = servlet.collectJsonFile("/" + path);
|
||||||
|
|
||||||
|
assertJson(jsonNodeOptional.orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertJson(JsonNode actual) throws IOException {
|
||||||
|
assertJson(actual.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertJson(String actual) throws IOException {
|
||||||
|
assertThat(actual)
|
||||||
|
.isNotEmpty()
|
||||||
|
.contains(StringUtils.deleteWhitespace(GIT_PLUGIN_JSON.substring(1, GIT_PLUGIN_JSON.length() - 1)))
|
||||||
|
.contains(StringUtils.deleteWhitespace(HG_PLUGIN_JSON.substring(1, HG_PLUGIN_JSON.length() - 1)))
|
||||||
|
.contains(StringUtils.deleteWhitespace(SVN_PLUGIN_JSON.substring(1, SVN_PLUGIN_JSON.length() - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public File createFileFromString(String json) throws IOException {
|
||||||
|
File file = temporaryFolder.newFile();
|
||||||
|
Files.write(json.getBytes(Charsets.UTF_8), file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user