mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 05:55:44 +01:00
merge 2.0.0-m3
This commit is contained in:
@@ -71,7 +71,7 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(T object) throws NotFoundException {
|
||||
public void delete(T object){
|
||||
decorated.delete(object);
|
||||
}
|
||||
|
||||
@@ -82,12 +82,12 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(T object) throws NotFoundException {
|
||||
public void modify(T object){
|
||||
decorated.modify(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh(T object) throws NotFoundException {
|
||||
public void refresh(T object){
|
||||
decorated.refresh(object);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package sonia.scm;
|
||||
|
||||
public class NotFoundException extends Exception {
|
||||
public class NotFoundException extends RuntimeException {
|
||||
public NotFoundException(String type, String id) {
|
||||
super(type + " with id '" + id + "' not found");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* <p>
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* <p>
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 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
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
@@ -24,13 +24,11 @@
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* <p>
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -40,12 +38,8 @@ import com.google.common.base.Objects;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlElementWrapper;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.Serializable;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -56,224 +50,56 @@ import java.util.List;
|
||||
*/
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "browser-result")
|
||||
public class BrowserResult implements Iterable<FileObject>, Serializable
|
||||
{
|
||||
public class BrowserResult implements Serializable {
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = 2818662048045182761L;
|
||||
private String revision;
|
||||
private FileObject file;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
public BrowserResult() {}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param revision
|
||||
* @param tag
|
||||
* @param branch
|
||||
* @param files
|
||||
*/
|
||||
public BrowserResult(String revision, String tag, String branch,
|
||||
List<FileObject> files)
|
||||
{
|
||||
this.revision = revision;
|
||||
this.tag = tag;
|
||||
this.branch = branch;
|
||||
this.files = files;
|
||||
public BrowserResult() {
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
public BrowserResult(String revision, FileObject file) {
|
||||
this.revision = revision;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public String getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
public FileObject getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
*
|
||||
* @param obj
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final BrowserResult other = (BrowserResult) obj;
|
||||
|
||||
return Objects.equal(revision, other.revision)
|
||||
&& Objects.equal(tag, other.tag)
|
||||
&& Objects.equal(branch, other.branch)
|
||||
&& Objects.equal(files, other.files);
|
||||
&& Objects.equal(file, other.file);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(revision, tag, branch, files);
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(revision, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Iterator<FileObject> iterator()
|
||||
{
|
||||
Iterator<FileObject> it = null;
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
it = files.iterator();
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
//J-
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("revision", revision)
|
||||
.add("tag", tag)
|
||||
.add("branch", branch)
|
||||
.add("files", files)
|
||||
.add("files", file)
|
||||
.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;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Strings;
|
||||
import sonia.scm.LastModifiedAware;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
@@ -44,8 +43,11 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import static java.util.Collections.unmodifiableCollection;
|
||||
|
||||
/**
|
||||
* The FileObject represents a file or a directory in a repository.
|
||||
@@ -181,6 +183,22 @@ public class FileObject implements LastModifiedAware, Serializable
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent path of the file.
|
||||
*
|
||||
* @return parent path
|
||||
*/
|
||||
public String getParentPath() {
|
||||
if (Strings.isNullOrEmpty(path)) {
|
||||
return null;
|
||||
}
|
||||
int index = path.lastIndexOf('/');
|
||||
if (index > 0) {
|
||||
return path.substring(0, index);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return sub repository informations or null if the file is not
|
||||
* sub repository.
|
||||
@@ -284,6 +302,22 @@ public class FileObject implements LastModifiedAware, Serializable
|
||||
this.subRepository = subRepository;
|
||||
}
|
||||
|
||||
public Collection<FileObject> getChildren() {
|
||||
return unmodifiableCollection(children);
|
||||
}
|
||||
|
||||
public void setChildren(List<FileObject> children) {
|
||||
this.children = new ArrayList<>(children);
|
||||
}
|
||||
|
||||
public void addChild(FileObject child) {
|
||||
this.children.add(child);
|
||||
}
|
||||
|
||||
public boolean hasChildren() {
|
||||
return !children.isEmpty();
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** file description */
|
||||
@@ -307,4 +341,6 @@ public class FileObject implements LastModifiedAware, Serializable
|
||||
/** sub repository informations */
|
||||
@XmlElement(name = "subrepository")
|
||||
private SubRepository subRepository;
|
||||
|
||||
private Collection<FileObject> children = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -161,11 +161,21 @@ public class PreProcessorUtil
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("prepare browser result of repository {} for return",
|
||||
repository.getName());
|
||||
logger.trace("prepare browser result of repository {} for return", repository.getName());
|
||||
}
|
||||
|
||||
handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
|
||||
PreProcessorHandler<FileObject> handler = new PreProcessorHandler<>(fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet, repository);
|
||||
handlePreProcessorForFileObject(handler, result.getFile());
|
||||
}
|
||||
|
||||
private void handlePreProcessorForFileObject(PreProcessorHandler<FileObject> handler, FileObject fileObject) {
|
||||
if (fileObject.isDirectory()) {
|
||||
for (FileObject child : fileObject.getChildren()) {
|
||||
handlePreProcessorForFileObject(handler, child);
|
||||
}
|
||||
}
|
||||
handler.callPreProcessorFactories(fileObject);
|
||||
handler.callPreProcessors(fileObject);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,11 +38,11 @@ package sonia.scm.repository.api;
|
||||
import com.google.common.base.Objects;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.FileObjectNameComparator;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryCacheKey;
|
||||
@@ -52,8 +52,6 @@ import sonia.scm.repository.spi.BrowseCommandRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -138,7 +136,7 @@ public final class BrowseCommandBuilder
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public BrowserResult getBrowserResult() throws IOException, RevisionNotFoundException {
|
||||
public BrowserResult getBrowserResult() throws IOException, NotFoundException {
|
||||
BrowserResult result = null;
|
||||
|
||||
if (disableCache)
|
||||
@@ -180,14 +178,6 @@ public final class BrowseCommandBuilder
|
||||
if (!disablePreProcessors && (result != null))
|
||||
{
|
||||
preProcessorUtil.prepareForReturn(repository, result);
|
||||
|
||||
List<FileObject> fileObjects = result.getFiles();
|
||||
|
||||
if (fileObjects != null)
|
||||
{
|
||||
Collections.sort(fileObjects, FileObjectNameComparator.instance);
|
||||
result.setFiles(fileObjects);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -35,8 +35,8 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -60,4 +60,5 @@ public interface BrowseCommand
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, RevisionNotFoundException;}
|
||||
BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, NotFoundException;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package sonia.scm.user;
|
||||
|
||||
public class ChangePasswordNotAllowedException extends RuntimeException {
|
||||
|
||||
public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password";
|
||||
public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password";
|
||||
|
||||
public ChangePasswordNotAllowedException(String message) {
|
||||
super(message);
|
||||
public ChangePasswordNotAllowedException(String type) {
|
||||
super(String.format(WRONG_USER_TYPE, type));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package sonia.scm.user;
|
||||
|
||||
public class InvalidPasswordException extends RuntimeException {
|
||||
|
||||
public static final String INVALID_MATCHING = "The given Password does not match with the stored one.";
|
||||
|
||||
public InvalidPasswordException(String message) {
|
||||
super(message);
|
||||
public InvalidPasswordException() {
|
||||
super("The given Password does not match with the stored one.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ import java.security.Principal;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"})
|
||||
@StaticPermissions(
|
||||
value = "user",
|
||||
globalPermissions = {"create", "list", "autocomplete"},
|
||||
permissions = {"read", "modify", "delete", "changePassword"})
|
||||
@XmlRootElement(name = "users")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
|
||||
@@ -274,10 +277,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
|
||||
//J+
|
||||
}
|
||||
|
||||
public User changePassword(String password){
|
||||
setPassword(password);
|
||||
return this;
|
||||
}
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,11 +38,7 @@ package sonia.scm.user;
|
||||
import sonia.scm.Manager;
|
||||
import sonia.scm.search.Searchable;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
|
||||
|
||||
/**
|
||||
* The central class for managing {@link User} objects.
|
||||
@@ -75,18 +71,6 @@ public interface UserManager
|
||||
*/
|
||||
public String getDefaultType();
|
||||
|
||||
|
||||
/**
|
||||
* Only account of the default type "xml" can change their password
|
||||
*/
|
||||
default Consumer<User> getUserTypeChecker() {
|
||||
return user -> {
|
||||
if (!isTypeDefault(user)) {
|
||||
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default boolean isTypeDefault(User user) {
|
||||
return getDefaultType().equals(user.getType());
|
||||
}
|
||||
@@ -99,5 +83,17 @@ public interface UserManager
|
||||
*/
|
||||
Collection<User> autocomplete(String filter);
|
||||
|
||||
/**
|
||||
* Changes the password of the logged in user.
|
||||
* @param oldPassword The current encrypted password of the user.
|
||||
* @param newPassword The new encrypted password of the user.
|
||||
*/
|
||||
void changePasswordForLoggedInUser(String oldPassword, String newPassword);
|
||||
|
||||
/**
|
||||
* Overwrites the password for the given user id. This needs user write privileges.
|
||||
* @param userId The id of the user to change the password for.
|
||||
* @param newPassword The new encrypted password.
|
||||
*/
|
||||
void overwritePassword(String userId, String newPassword);
|
||||
}
|
||||
|
||||
@@ -126,7 +126,16 @@ public class UserManagerDecorator extends ManagerDecorator<User>
|
||||
return decorated.autocomplete(filter);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
@Override
|
||||
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
|
||||
decorated.changePasswordForLoggedInUser(oldPassword, newPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwritePassword(String userId, String newPassword) {
|
||||
decorated.overwritePassword(userId, newPassword);
|
||||
}
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final UserManager decorated;
|
||||
|
||||
@@ -39,6 +39,8 @@ public class VndMediaType {
|
||||
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
||||
@SuppressWarnings("squid:S2068")
|
||||
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
||||
@SuppressWarnings("squid:S2068")
|
||||
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
|
||||
|
||||
public static final String ME = PREFIX + "me" + SUFFIX;
|
||||
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,7 @@ public class SyncingRealmHelperTest {
|
||||
* Tests {@link SyncingRealmHelper#store(Group)} with an existing group.
|
||||
*/
|
||||
@Test
|
||||
public void testStoreGroupModify() throws NotFoundException {
|
||||
public void testStoreGroupModify(){
|
||||
Group group = new Group("unit-test", "heartOfGold");
|
||||
|
||||
when(groupManager.get("heartOfGold")).thenReturn(group);
|
||||
@@ -191,7 +191,7 @@ public class SyncingRealmHelperTest {
|
||||
* Tests {@link SyncingRealmHelper#store(User)} with an existing user.
|
||||
*/
|
||||
@Test
|
||||
public void testStoreUserModify() throws NotFoundException {
|
||||
public void testStoreUserModify(){
|
||||
when(userManager.contains("tricia")).thenReturn(Boolean.TRUE);
|
||||
|
||||
User user = new User("tricia");
|
||||
|
||||
73
scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java
Normal file
73
scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.ScmRequests;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class AutoCompleteITCase {
|
||||
|
||||
|
||||
public static final String CREATED_USER_PREFIX = "user_";
|
||||
public static final String CREATED_GROUP_PREFIX = "group_";
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
TestData.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminShouldAutoComplete() {
|
||||
shouldAutocomplete(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userShouldAutoComplete() {
|
||||
String username = "nonAdmin";
|
||||
String password = "pass";
|
||||
TestData.createUser(username, password, false, "xml", "email@e.de");
|
||||
shouldAutocomplete(username, password);
|
||||
}
|
||||
|
||||
public void shouldAutocomplete(String username, String password) {
|
||||
createUsers();
|
||||
createGroups();
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(username, password)
|
||||
.assertStatusCode(200)
|
||||
.requestAutoCompleteGroups("group*")
|
||||
.assertStatusCode(200)
|
||||
.assertAutoCompleteResults(assertAutoCompleteResult(CREATED_GROUP_PREFIX))
|
||||
.returnToPrevious()
|
||||
.requestAutoCompleteUsers("user*")
|
||||
.assertStatusCode(200)
|
||||
.assertAutoCompleteResults(assertAutoCompleteResult(CREATED_USER_PREFIX));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Consumer<List<Map>> assertAutoCompleteResult(String id) {
|
||||
return autoCompleteDtos -> {
|
||||
IntStream.range(0, 5).forEach(i -> {
|
||||
assertThat(autoCompleteDtos).as("return maximum 5 entries").hasSize(5);
|
||||
assertThat(autoCompleteDtos.get(i)).containsEntry("id", id + (i + 1));
|
||||
assertThat(autoCompleteDtos.get(i)).containsEntry("displayName", id + (i + 1));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private void createUsers() {
|
||||
IntStream.range(0, 6).forEach(i -> TestData.createUser(CREATED_USER_PREFIX + (i + 1), "pass", false, "xml", CREATED_USER_PREFIX + (i + 1) + "@scm-manager.org"));
|
||||
}
|
||||
|
||||
private void createGroups() {
|
||||
IntStream.range(0, 6).forEach(i -> TestData.createGroup(CREATED_GROUP_PREFIX + (i + 1), CREATED_GROUP_PREFIX + (i + 1)));
|
||||
}
|
||||
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,9 @@ public class MeITCase {
|
||||
String newPassword = TestData.USER_SCM_ADMIN + "1";
|
||||
// admin change the own password
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
|
||||
.getMeResource()
|
||||
.requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
|
||||
.requestMe()
|
||||
.assertStatusCode(200)
|
||||
.usingMeResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo("xml"))
|
||||
@@ -33,30 +30,48 @@ public class MeITCase {
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password than undo changes
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(TestData.USER_SCM_ADMIN))
|
||||
.usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
|
||||
.getMeResource()
|
||||
.requestIndexResource(TestData.USER_SCM_ADMIN, newPassword)
|
||||
.requestMe()
|
||||
.assertStatusCode(200)
|
||||
.usingMeResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
|
||||
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
|
||||
.assertStatusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminUserShouldChangeOwnPassword() {
|
||||
String newPassword = "pass1";
|
||||
String username = "user1";
|
||||
String password = "pass";
|
||||
TestData.createUser(username, password,false,"xml", "em@l.de");
|
||||
// user change the own password
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(username, password)
|
||||
.requestMe()
|
||||
.assertStatusCode(200)
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.FALSE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo("xml"))
|
||||
.requestChangePassword(password, newPassword)
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password than undo changes
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(username, newPassword)
|
||||
.requestMe()
|
||||
.assertStatusCode(200);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "not XML Type";
|
||||
TestData.createUser(newUser, password, true, type);
|
||||
TestData.createUser(newUser, password, true, type, "user@scm-manager.org");
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getMeResource()
|
||||
.requestIndexResource(newUser, password)
|
||||
.requestMe()
|
||||
.assertStatusCode(200)
|
||||
.usingMeResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
|
||||
@@ -87,13 +87,13 @@ public class PermissionsITCase {
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
TestData.createDefault();
|
||||
TestData.createUser(USER_READ, USER_PASS);
|
||||
TestData.createNotAdminUser(USER_READ, USER_PASS);
|
||||
TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType);
|
||||
TestData.createUser(USER_WRITE, USER_PASS);
|
||||
TestData.createNotAdminUser(USER_WRITE, USER_PASS);
|
||||
TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType);
|
||||
TestData.createUser(USER_OWNER, USER_PASS);
|
||||
TestData.createNotAdminUser(USER_OWNER, USER_PASS);
|
||||
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
|
||||
TestData.createUser(USER_OTHER, USER_PASS);
|
||||
TestData.createNotAdminUser(USER_OTHER, USER_PASS);
|
||||
createdPermissions = 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class RepositoryAccessITCase {
|
||||
|
||||
private final String repositoryType;
|
||||
private File folder;
|
||||
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
|
||||
private ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> repositoryResponse;
|
||||
|
||||
public RepositoryAccessITCase(String repositoryType) {
|
||||
this.repositoryType = repositoryType;
|
||||
@@ -59,17 +59,13 @@ public class RepositoryAccessITCase {
|
||||
public void init() {
|
||||
TestData.createDefault();
|
||||
folder = tempFolder.getRoot();
|
||||
repositoryGetRequest = ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getDefaultRepositoryUrl(repositoryType))
|
||||
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.getRepositoryResource()
|
||||
String namespace = ADMIN_USERNAME;
|
||||
String repo = TestData.getDefaultRepoName(repositoryType);
|
||||
repositoryResponse =
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.requestRepository(namespace, repo)
|
||||
.assertStatusCode(HttpStatus.SC_OK);
|
||||
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
.getMeResource();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -201,7 +197,7 @@ public class RepositoryAccessITCase {
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.extract()
|
||||
.path("_embedded.files.find{it.name=='a.txt'}._links.self.href");
|
||||
.path("_embedded.children.find{it.name=='a.txt'}._links.self.href");
|
||||
|
||||
given()
|
||||
.when()
|
||||
@@ -216,7 +212,7 @@ public class RepositoryAccessITCase {
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.extract()
|
||||
.path("_embedded.files.find{it.name=='subfolder'}._links.self.href");
|
||||
.path("_embedded.children.find{it.name=='subfolder'}._links.self.href");
|
||||
String selfOfSubfolderUrl = given()
|
||||
.when()
|
||||
.get(subfolderSourceUrl)
|
||||
@@ -231,7 +227,7 @@ public class RepositoryAccessITCase {
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.extract()
|
||||
.path("_embedded.files[0]._links.self.href");
|
||||
.path("_embedded.children[0]._links.self.href");
|
||||
given()
|
||||
.when()
|
||||
.get(subfolderContentUrl)
|
||||
@@ -306,17 +302,12 @@ public class RepositoryAccessITCase {
|
||||
public void shouldFindFileHistory() throws IOException {
|
||||
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
|
||||
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a");
|
||||
repositoryGetRequest
|
||||
.usingRepositoryResponse()
|
||||
repositoryResponse
|
||||
.requestSources()
|
||||
.usingSourcesResponse()
|
||||
.requestSelf("folder")
|
||||
.usingSourcesResponse()
|
||||
.requestSelf("subfolder")
|
||||
.usingSourcesResponse()
|
||||
.requestFileHistory("a.txt")
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingChangesetsResponse()
|
||||
.assertChangesets(changesets -> {
|
||||
assertThat(changesets).hasSize(1);
|
||||
assertThat(changesets.get(0)).containsEntry("id", changeset.getId());
|
||||
@@ -332,14 +323,11 @@ public class RepositoryAccessITCase {
|
||||
String fileName = "a.txt";
|
||||
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
|
||||
String revision = changeset.getId();
|
||||
repositoryGetRequest
|
||||
.usingRepositoryResponse()
|
||||
repositoryResponse
|
||||
.requestChangesets()
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingChangesetsResponse()
|
||||
.requestModifications(revision)
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingModificationsResponse()
|
||||
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
|
||||
.assertAdded(addedFiles -> assertThat(addedFiles)
|
||||
.hasSize(1)
|
||||
@@ -359,14 +347,11 @@ public class RepositoryAccessITCase {
|
||||
Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName);
|
||||
|
||||
String revision = changeset.getId();
|
||||
repositoryGetRequest
|
||||
.usingRepositoryResponse()
|
||||
repositoryResponse
|
||||
.requestChangesets()
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingChangesetsResponse()
|
||||
.requestModifications(revision)
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingModificationsResponse()
|
||||
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
|
||||
.assertRemoved(removedFiles -> assertThat(removedFiles)
|
||||
.hasSize(1)
|
||||
@@ -386,14 +371,11 @@ public class RepositoryAccessITCase {
|
||||
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content");
|
||||
|
||||
String revision = changeset.getId();
|
||||
repositoryGetRequest
|
||||
.usingRepositoryResponse()
|
||||
repositoryResponse
|
||||
.requestChangesets()
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingChangesetsResponse()
|
||||
.requestModifications(revision)
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingModificationsResponse()
|
||||
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
|
||||
.assertModified(modifiedFiles -> assertThat(modifiedFiles)
|
||||
.hasSize(1)
|
||||
@@ -423,14 +405,11 @@ public class RepositoryAccessITCase {
|
||||
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
|
||||
|
||||
String revision = changeset.getId();
|
||||
repositoryGetRequest
|
||||
.usingRepositoryResponse()
|
||||
repositoryResponse
|
||||
.requestChangesets()
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingChangesetsResponse()
|
||||
.requestModifications(revision)
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingModificationsResponse()
|
||||
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
|
||||
.assertAdded(a -> assertThat(a)
|
||||
.hasSize(1)
|
||||
@@ -463,14 +442,11 @@ public class RepositoryAccessITCase {
|
||||
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
|
||||
|
||||
String revision = changeset.getId();
|
||||
repositoryGetRequest
|
||||
.usingRepositoryResponse()
|
||||
repositoryResponse
|
||||
.requestChangesets()
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingChangesetsResponse()
|
||||
.requestModifications(revision)
|
||||
.assertStatusCode(HttpStatus.SC_OK)
|
||||
.usingModificationsResponse()
|
||||
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
|
||||
.assertAdded(a -> assertThat(a)
|
||||
.hasSize(3)
|
||||
|
||||
@@ -19,75 +19,83 @@ public class UserITCase {
|
||||
public void adminShouldChangeOwnPassword() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
TestData.createUser(newUser, password, true, "xml");
|
||||
TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org");
|
||||
String newPassword = "new_password";
|
||||
// admin change the own password
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getUserResource()
|
||||
.requestIndexResource(newUser, password)
|
||||
.assertStatusCode(200)
|
||||
.requestUser(newUser)
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
|
||||
.requestChangePassword(newPassword)
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))
|
||||
.usernameAndPassword(newUser, newPassword)
|
||||
.getUserResource()
|
||||
.requestIndexResource(newUser, newPassword)
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.requestUser(newUser)
|
||||
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adminShouldChangePasswordOfOtherUser() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
TestData.createUser(newUser, password, true, "xml");
|
||||
TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org");
|
||||
String newPassword = "new_password";
|
||||
// admin change the password of the user
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))// the admin get the user object
|
||||
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
|
||||
.getUserResource()
|
||||
.requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
|
||||
.assertStatusCode(200)
|
||||
.requestUser(newUser)
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
|
||||
.assertPassword(Assert::assertNull)
|
||||
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getUserUrl(newUser))
|
||||
.usernameAndPassword(newUser, newPassword)
|
||||
.getUserResource()
|
||||
.requestIndexResource(newUser, newPassword)
|
||||
.assertStatusCode(200)
|
||||
.requestUser(newUser)
|
||||
.assertStatusCode(200);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminUserShouldNotChangePasswordOfOtherUser() {
|
||||
String user = "user";
|
||||
String password = "pass";
|
||||
TestData.createUser(user, password, false, "xml", "em@l.de");
|
||||
String user2 = "user2";
|
||||
TestData.createUser(user2, password, false, "xml", "em@l.de");
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(user, password)
|
||||
.assertUsersLinkDoesNotExists();
|
||||
// use the users/ endpoint bypassed the index resource
|
||||
ScmRequests.start()
|
||||
.requestUser(user, password, user2)
|
||||
.assertStatusCode(403);
|
||||
// use the users/password endpoint bypassed the index and users resources
|
||||
ScmRequests.start()
|
||||
.requestUserChangePassword(user, password, user2, "newPassword")
|
||||
.assertStatusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "not XML Type";
|
||||
TestData.createUser(newUser, password, true, type);
|
||||
TestData.createUser(newUser, password, true, type, "user@scm-manager.org");
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getUserResource()
|
||||
.requestIndexResource(newUser, password)
|
||||
.assertStatusCode(200)
|
||||
.requestUser(newUser)
|
||||
.assertStatusCode(200)
|
||||
.usingUserResponse()
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
|
||||
@@ -2,9 +2,13 @@ package sonia.scm.it.utils;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
import org.junit.Assert;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.ConnectException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
@@ -25,7 +29,8 @@ import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
|
||||
*/
|
||||
public class ScmRequests {
|
||||
|
||||
private String url;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScmRequests.class);
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
@@ -33,10 +38,29 @@ public class ScmRequests {
|
||||
return new ScmRequests();
|
||||
}
|
||||
|
||||
public Given given() {
|
||||
return new Given();
|
||||
public IndexResponse requestIndexResource(String username, String password) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
|
||||
}
|
||||
|
||||
public <SELF extends UserResponse<SELF, T>, T extends ModelResponse> UserResponse<SELF,T> requestUser(String username, String password, String pathParam) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/"+pathParam).toString()), null);
|
||||
}
|
||||
|
||||
public ChangePasswordResponse<ChangePasswordResponse> requestUserChangePassword(String username, String password, String userPathParam, String newPassword) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the extracted url from the given link
|
||||
@@ -46,24 +70,54 @@ public class ScmRequests {
|
||||
* @return the response of the GET request using the given link
|
||||
*/
|
||||
private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
|
||||
return applyGETRequest(response
|
||||
.then()
|
||||
.extract()
|
||||
.path(linkPropertyName));
|
||||
return applyGETRequestFromLinkWithParams(response, linkPropertyName, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the extracted url from the given link
|
||||
*
|
||||
* @param linkPropertyName the property name of link
|
||||
* @param response the response containing the link
|
||||
* @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name
|
||||
* @return the response of the GET request using the given link
|
||||
*/
|
||||
private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) {
|
||||
String url = response
|
||||
.then()
|
||||
.extract()
|
||||
.path(linkPropertyName);
|
||||
Assert.assertNotNull("no url found for link " + linkPropertyName, url);
|
||||
return applyGETRequestWithQueryParams(url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the GET request
|
||||
* @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name
|
||||
* @return the response of the GET request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyGETRequestWithQueryParams(String url, String params) {
|
||||
LOG.info("GET {}", url);
|
||||
if (username == null || password == null){
|
||||
return RestAssured.given()
|
||||
.when()
|
||||
.get(url + params);
|
||||
}
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
.get(url + params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the given <code>url</code> and return the response.
|
||||
*
|
||||
* @param url the url of the GET request
|
||||
* @return the response of the GET request using the given <code>url</code>
|
||||
*/
|
||||
**/
|
||||
private Response applyGETRequest(String url) {
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
.get(url);
|
||||
return applyGETRequestWithQueryParams(url, "");
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +146,7 @@ public class ScmRequests {
|
||||
* @return the response of the PUT request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyPUTRequest(String url, String mediaType, String body) {
|
||||
LOG.info("PUT {}", url);
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
@@ -101,11 +156,6 @@ public class ScmRequests {
|
||||
.put(url);
|
||||
}
|
||||
|
||||
|
||||
private void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
@@ -114,272 +164,163 @@ public class ScmRequests {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return url;
|
||||
public class IndexResponse extends ModelResponse<IndexResponse, IndexResponse> {
|
||||
public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href";
|
||||
public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href";
|
||||
public static final String LINK_REPOSITORIES = "_links.repositories.href";
|
||||
private static final String LINK_ME = "_links.me.href";
|
||||
private static final String LINK_USERS = "_links.users.href";
|
||||
|
||||
public IndexResponse(Response response) {
|
||||
super(response, null);
|
||||
}
|
||||
|
||||
private String getUsername() {
|
||||
return username;
|
||||
public AutoCompleteResponse<IndexResponse> requestAutoCompleteUsers(String q) {
|
||||
return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_USERS, "?q=" + q), this);
|
||||
}
|
||||
|
||||
private String getPassword() {
|
||||
return password;
|
||||
public AutoCompleteResponse<IndexResponse> requestAutoCompleteGroups(String q) {
|
||||
return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_GROUPS, "?q=" + q), this);
|
||||
}
|
||||
|
||||
public class Given {
|
||||
|
||||
public GivenUrl url(String url) {
|
||||
setUrl(url);
|
||||
return new GivenUrl();
|
||||
public RepositoryResponse<IndexResponse> requestRepository(String namespace, String name) {
|
||||
return new RepositoryResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORIES, namespace + "/" + name), this);
|
||||
}
|
||||
|
||||
public GivenUrl url(URI url) {
|
||||
setUrl(url.toString());
|
||||
return new GivenUrl();
|
||||
public MeResponse<IndexResponse> requestMe() {
|
||||
return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this);
|
||||
}
|
||||
|
||||
public UserResponse<? extends UserResponse, IndexResponse> requestUser(String username) {
|
||||
return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this);
|
||||
}
|
||||
|
||||
public IndexResponse assertUsersLinkDoesNotExists() {
|
||||
return super.assertPropertyPathDoesNotExists(LINK_USERS);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class RepositoryResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> {
|
||||
|
||||
|
||||
public static final String LINKS_SOURCES = "_links.sources.href";
|
||||
public static final String LINKS_CHANGESETS = "_links.changesets.href";
|
||||
|
||||
public RepositoryResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public SourcesResponse<RepositoryResponse> requestSources() {
|
||||
return new SourcesResponse<>(applyGETRequestFromLink(response, LINKS_SOURCES), this);
|
||||
}
|
||||
|
||||
public ChangesetsResponse<RepositoryResponse> requestChangesets() {
|
||||
return new ChangesetsResponse<>(applyGETRequestFromLink(response, LINKS_CHANGESETS), this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class GivenWithUrlAndAuth {
|
||||
public AppliedMeRequest getMeResource() {
|
||||
return new AppliedMeRequest(applyGETRequest(url));
|
||||
}
|
||||
public class ChangesetsResponse<PREV extends ModelResponse> extends ModelResponse<ChangesetsResponse<PREV>, PREV> {
|
||||
|
||||
public AppliedUserRequest getUserResource() {
|
||||
return new AppliedUserRequest(applyGETRequest(url));
|
||||
}
|
||||
|
||||
public AppliedRepositoryRequest getRepositoryResource() {
|
||||
return new AppliedRepositoryRequest(
|
||||
applyGETRequest(url)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedRequest<SELF extends AppliedRequest> {
|
||||
private Response response;
|
||||
|
||||
public AppliedRequest(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* apply custom assertions to the actual response
|
||||
*
|
||||
* @param consumer consume the response in order to assert the content. the header, the payload etc..
|
||||
* @return the self object
|
||||
*/
|
||||
public SELF assertResponse(Consumer<Response> consumer) {
|
||||
consumer.accept(response);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* special assertion of the status code
|
||||
*
|
||||
* @param expectedStatusCode the expected status code
|
||||
* @return the self object
|
||||
*/
|
||||
public SELF assertStatusCode(int expectedStatusCode) {
|
||||
this.response.then().assertThat().statusCode(expectedStatusCode);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AppliedRepositoryRequest extends AppliedRequest<AppliedRepositoryRequest> {
|
||||
|
||||
public AppliedRepositoryRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public RepositoryResponse usingRepositoryResponse() {
|
||||
return new RepositoryResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
public class RepositoryResponse {
|
||||
|
||||
private Response repositoryResponse;
|
||||
|
||||
public RepositoryResponse(Response repositoryResponse) {
|
||||
this.repositoryResponse = repositoryResponse;
|
||||
}
|
||||
|
||||
public AppliedSourcesRequest requestSources() {
|
||||
return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href"));
|
||||
}
|
||||
|
||||
public AppliedChangesetsRequest requestChangesets() {
|
||||
return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href"));
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedChangesetsRequest extends AppliedRequest<AppliedChangesetsRequest> {
|
||||
|
||||
public AppliedChangesetsRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public ChangesetsResponse usingChangesetsResponse() {
|
||||
return new ChangesetsResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangesetsResponse {
|
||||
private Response changesetsResponse;
|
||||
|
||||
public ChangesetsResponse(Response changesetsResponse) {
|
||||
this.changesetsResponse = changesetsResponse;
|
||||
public ChangesetsResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
|
||||
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
|
||||
List<Map> changesets = response.then().extract().path("_embedded.changesets");
|
||||
changesetsConsumer.accept(changesets);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppliedDiffRequest requestDiff(String revision) {
|
||||
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
|
||||
public DiffResponse<ChangesetsResponse> requestDiff(String revision) {
|
||||
return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this);
|
||||
}
|
||||
|
||||
public AppliedModificationsRequest requestModifications(String revision) {
|
||||
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
|
||||
public ModificationsResponse<ChangesetsResponse> requestModifications(String revision) {
|
||||
return new ModificationsResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"), this);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedSourcesRequest extends AppliedRequest<AppliedSourcesRequest> {
|
||||
|
||||
public AppliedSourcesRequest(Response sourcesResponse) {
|
||||
super(sourcesResponse);
|
||||
}
|
||||
public class SourcesResponse<PREV extends ModelResponse> extends ModelResponse<SourcesResponse<PREV>, PREV> {
|
||||
|
||||
public SourcesResponse usingSourcesResponse() {
|
||||
return new SourcesResponse(super.response);
|
||||
}
|
||||
}
|
||||
|
||||
public class SourcesResponse {
|
||||
|
||||
private Response sourcesResponse;
|
||||
|
||||
public SourcesResponse(Response sourcesResponse) {
|
||||
this.sourcesResponse = sourcesResponse;
|
||||
public SourcesResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public SourcesResponse assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = sourcesResponse.then().extract().path("revision");
|
||||
String revision = response.then().extract().path("revision");
|
||||
assertRevision.accept(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourcesResponse assertFiles(Consumer<List> assertFiles) {
|
||||
List files = sourcesResponse.then().extract().path("files");
|
||||
List files = response.then().extract().path("files");
|
||||
assertFiles.accept(files);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppliedChangesetsRequest requestFileHistory(String fileName) {
|
||||
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
|
||||
public ChangesetsResponse<SourcesResponse> requestFileHistory(String fileName) {
|
||||
return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.history.href"), this);
|
||||
}
|
||||
|
||||
public AppliedSourcesRequest requestSelf(String fileName) {
|
||||
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
|
||||
public SourcesResponse<SourcesResponse> requestSelf(String fileName) {
|
||||
return new SourcesResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.self.href"), this);
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedDiffRequest extends AppliedRequest<AppliedDiffRequest> {
|
||||
public class ModificationsResponse<PREV extends ModelResponse> extends ModelResponse<ModificationsResponse<PREV>, PREV> {
|
||||
|
||||
public AppliedDiffRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
public ModificationsResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public class GivenUrl {
|
||||
|
||||
public GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new GivenWithUrlAndAuth();
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedModificationsRequest extends AppliedRequest<AppliedModificationsRequest> {
|
||||
public AppliedModificationsRequest(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public ModificationsResponse usingModificationsResponse() {
|
||||
return new ModificationsResponse(super.response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ModificationsResponse {
|
||||
private Response resource;
|
||||
|
||||
public ModificationsResponse(Response resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = resource.then().extract().path("revision");
|
||||
public ModificationsResponse<PREV> assertRevision(Consumer<String> assertRevision) {
|
||||
String revision = response.then().extract().path("revision");
|
||||
assertRevision.accept(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
|
||||
List<String> added = resource.then().extract().path("added");
|
||||
public ModificationsResponse<PREV> assertAdded(Consumer<List<String>> assertAdded) {
|
||||
List<String> added = response.then().extract().path("added");
|
||||
assertAdded.accept(added);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
|
||||
List<String> removed = resource.then().extract().path("removed");
|
||||
public ModificationsResponse<PREV> assertRemoved(Consumer<List<String>> assertRemoved) {
|
||||
List<String> removed = response.then().extract().path("removed");
|
||||
assertRemoved.accept(removed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
|
||||
List<String> modified = resource.then().extract().path("modified");
|
||||
public ModificationsResponse<PREV> assertModified(Consumer<List<String>> assertModified) {
|
||||
List<String> modified = response.then().extract().path("modified");
|
||||
assertModified.accept(modified);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AppliedMeRequest extends AppliedRequest<AppliedMeRequest> {
|
||||
public class MeResponse<PREV extends ModelResponse> extends UserResponse<MeResponse<PREV>, PREV> {
|
||||
|
||||
public AppliedMeRequest(Response response) {
|
||||
super(response);
|
||||
|
||||
public MeResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public MeResponse usingMeResponse() {
|
||||
return new MeResponse(super.response);
|
||||
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
|
||||
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class MeResponse extends UserResponse<MeResponse> {
|
||||
|
||||
|
||||
public MeResponse(Response response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) {
|
||||
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class UserResponse<SELF extends UserResponse> extends ModelResponse<SELF> {
|
||||
public class UserResponse<SELF extends UserResponse<SELF, PREV>, PREV extends ModelResponse> extends ModelResponse<SELF, PREV> {
|
||||
|
||||
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
|
||||
|
||||
public UserResponse(Response response) {
|
||||
super(response);
|
||||
public UserResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public SELF assertPassword(Consumer<String> assertPassword) {
|
||||
@@ -402,22 +343,27 @@ public class ScmRequests {
|
||||
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
|
||||
}
|
||||
|
||||
public AppliedChangePasswordRequest requestChangePassword(String newPassword) {
|
||||
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword)));
|
||||
public ChangePasswordResponse<UserResponse> requestChangePassword(String newPassword) {
|
||||
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_OVERWRITE, createPasswordChangeJson(null, newPassword)), this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* encapsulate standard assertions over model properties
|
||||
*/
|
||||
public class ModelResponse<SELF extends ModelResponse> {
|
||||
public class ModelResponse<SELF extends ModelResponse<SELF, PREV>, PREV extends ModelResponse> {
|
||||
|
||||
protected PREV previousResponse;
|
||||
protected Response response;
|
||||
|
||||
public ModelResponse(Response response) {
|
||||
public ModelResponse(Response response, PREV previousResponse) {
|
||||
this.response = response;
|
||||
this.previousResponse = previousResponse;
|
||||
}
|
||||
|
||||
public PREV returnToPrevious() {
|
||||
return previousResponse;
|
||||
}
|
||||
|
||||
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
|
||||
@@ -441,25 +387,45 @@ public class ScmRequests {
|
||||
assertProperties.accept(properties);
|
||||
return (SELF) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* special assertion of the status code
|
||||
*
|
||||
* @param expectedStatusCode the expected status code
|
||||
* @return the self object
|
||||
*/
|
||||
public SELF assertStatusCode(int expectedStatusCode) {
|
||||
this.response.then().assertThat().statusCode(expectedStatusCode);
|
||||
return (SELF) this;
|
||||
}
|
||||
}
|
||||
|
||||
public class AppliedChangePasswordRequest extends AppliedRequest<AppliedChangePasswordRequest> {
|
||||
public class AutoCompleteResponse<PREV extends ModelResponse> extends ModelResponse<AutoCompleteResponse<PREV>, PREV> {
|
||||
|
||||
public AppliedChangePasswordRequest(Response response) {
|
||||
super(response);
|
||||
public AutoCompleteResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public AutoCompleteResponse<PREV> assertAutoCompleteResults(Consumer<List<Map>> checker) {
|
||||
List<Map> result = response.then().extract().path("");
|
||||
checker.accept(result);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AppliedUserRequest extends AppliedRequest<AppliedUserRequest> {
|
||||
|
||||
public AppliedUserRequest(Response response) {
|
||||
super(response);
|
||||
public class DiffResponse<PREV extends ModelResponse> extends ModelResponse<DiffResponse<PREV>, PREV> {
|
||||
|
||||
public DiffResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
}
|
||||
|
||||
public UserResponse usingUserResponse() {
|
||||
return new UserResponse(super.response);
|
||||
}
|
||||
public class ChangePasswordResponse<PREV extends ModelResponse> extends ModelResponse<ChangePasswordResponse<PREV>, PREV> {
|
||||
|
||||
public ChangePasswordResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ public class TestData {
|
||||
return DEFAULT_REPOSITORIES.get(repositoryType);
|
||||
}
|
||||
|
||||
public static void createUser(String username, String password) {
|
||||
createUser(username, password, false, "xml");
|
||||
public static void createNotAdminUser(String username, String password) {
|
||||
createUser(username, password, false, "xml", "user1@scm-manager.org");
|
||||
}
|
||||
|
||||
public static void createUser(String username, String password, boolean isAdmin, String type) {
|
||||
public static void createUser(String username, String password, boolean isAdmin, String type, final String email) {
|
||||
LOG.info("create user with username: {}", username);
|
||||
String admin = isAdmin ? "true" : "false";
|
||||
given(VndMediaType.USER)
|
||||
@@ -61,7 +61,7 @@ public class TestData {
|
||||
.append(" \"admin\": ").append(admin).append(",\n")
|
||||
.append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
|
||||
.append(" \"displayName\": \"").append(username).append("\",\n")
|
||||
.append(" \"mail\": \"user1@scm-manager.org\",\n")
|
||||
.append(" \"mail\": \"" + email + "\",\n")
|
||||
.append(" \"name\": \"").append(username).append("\",\n")
|
||||
.append(" \"password\": \"").append(password).append("\",\n")
|
||||
.append(" \"type\": \"").append(type).append("\"\n")
|
||||
@@ -71,6 +71,16 @@ public class TestData {
|
||||
.statusCode(HttpStatus.SC_CREATED)
|
||||
;
|
||||
}
|
||||
public static void createGroup(String groupName, String desc) {
|
||||
LOG.info("create group with group name: {} and description {}", groupName, desc);
|
||||
given(VndMediaType.GROUP)
|
||||
.when()
|
||||
.content(getGroupJson(groupName,desc))
|
||||
.post(getGroupsUrl())
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_CREATED)
|
||||
;
|
||||
}
|
||||
|
||||
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
|
||||
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
|
||||
@@ -193,28 +203,31 @@ public class TestData {
|
||||
return JSON_BUILDER
|
||||
.add("contact", "zaphod.beeblebrox@hitchhiker.com")
|
||||
.add("description", "Heart of Gold")
|
||||
.add("name", "HeartOfGold-" + repositoryType)
|
||||
.add("name", getDefaultRepoName(repositoryType))
|
||||
.add("archived", false)
|
||||
.add("type", repositoryType)
|
||||
.build().toString();
|
||||
}
|
||||
|
||||
public static URI getMeUrl() {
|
||||
return RestUtil.createResourceUrl("me/");
|
||||
public static String getDefaultRepoName(String repositoryType) {
|
||||
return "HeartOfGold-" + repositoryType;
|
||||
}
|
||||
|
||||
public static String getGroupJson(String groupname , String desc) {
|
||||
return JSON_BUILDER
|
||||
.add("name", groupname)
|
||||
.add("description", desc)
|
||||
.build().toString();
|
||||
}
|
||||
|
||||
public static URI getGroupsUrl() {
|
||||
return RestUtil.createResourceUrl("groups/");
|
||||
}
|
||||
|
||||
public static URI getUsersUrl() {
|
||||
return RestUtil.createResourceUrl("users/");
|
||||
|
||||
}
|
||||
|
||||
public static URI getUserUrl(String username) {
|
||||
return getUsersUrl().resolve(username);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static String createPasswordChangeJson(String oldPassword, String newPassword) {
|
||||
return JSON_BUILDER
|
||||
.add("oldPassword", oldPassword)
|
||||
@@ -225,4 +238,5 @@ public class TestData {
|
||||
public static void main(String[] args) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import org.eclipse.jgit.errors.MissingObjectException;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectLoader;
|
||||
@@ -50,6 +50,7 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
|
||||
import org.eclipse.jgit.treewalk.filter.TreeFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.GitSubModuleParser;
|
||||
@@ -103,10 +104,11 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public BrowserResult getBrowserResult(BrowseCommandRequest request)
|
||||
throws IOException, RevisionNotFoundException {
|
||||
throws IOException, NotFoundException {
|
||||
logger.debug("try to create browse result for {}", request);
|
||||
|
||||
BrowserResult result;
|
||||
|
||||
org.eclipse.jgit.lib.Repository repo = open();
|
||||
ObjectId revId;
|
||||
|
||||
@@ -121,7 +123,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
|
||||
if (revId != null)
|
||||
{
|
||||
result = getResult(repo, request, revId);
|
||||
result = new BrowserResult(revId.getName(), getEntry(repo, request, revId));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -134,8 +136,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
logger.warn("coul not find head of repository, empty?");
|
||||
}
|
||||
|
||||
result = new BrowserResult(Constants.HEAD, null, null,
|
||||
Collections.EMPTY_LIST);
|
||||
result = new BrowserResult(Constants.HEAD, createEmtpyRoot());
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -143,6 +144,14 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
private FileObject createEmtpyRoot() {
|
||||
FileObject fileObject = new FileObject();
|
||||
fileObject.setName("");
|
||||
fileObject.setPath("");
|
||||
fileObject.setDirectory(true);
|
||||
return fileObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -158,11 +167,8 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
|
||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
|
||||
throws IOException, RevisionNotFoundException {
|
||||
FileObject file;
|
||||
|
||||
try
|
||||
{
|
||||
file = new FileObject();
|
||||
FileObject file = new FileObject();
|
||||
|
||||
String path = treeWalk.getPathString();
|
||||
|
||||
@@ -193,7 +199,6 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
if (!file.isDirectory() &&!request.isDisableLastCommit())
|
||||
{
|
||||
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
||||
|
||||
RevCommit commit = getLatestCommit(repo, revId, path);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -265,22 +258,19 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
return result;
|
||||
}
|
||||
|
||||
private BrowserResult getResult(org.eclipse.jgit.lib.Repository repo,
|
||||
BrowseCommandRequest request, ObjectId revId)
|
||||
throws IOException, RevisionNotFoundException {
|
||||
BrowserResult result = null;
|
||||
private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException, NotFoundException {
|
||||
RevWalk revWalk = null;
|
||||
TreeWalk treeWalk = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
FileObject result;
|
||||
|
||||
try {
|
||||
logger.debug("load repository browser for revision {}", revId.name());
|
||||
}
|
||||
|
||||
treeWalk = new TreeWalk(repo);
|
||||
treeWalk.setRecursive(request.isRecursive());
|
||||
if (!isRootRequest(request)) {
|
||||
treeWalk.setFilter(PathFilter.create(request.getPath()));
|
||||
}
|
||||
revWalk = new RevWalk(repo);
|
||||
|
||||
RevTree tree = revWalk.parseTree(revId);
|
||||
@@ -291,65 +281,20 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error("could not find tree for {}", revId.name());
|
||||
throw new IllegalStateException("could not find tree for " + revId.name());
|
||||
}
|
||||
|
||||
result = new BrowserResult();
|
||||
|
||||
List<FileObject> files = Lists.newArrayList();
|
||||
|
||||
String path = request.getPath();
|
||||
|
||||
if (Util.isEmpty(path))
|
||||
{
|
||||
while (treeWalk.next())
|
||||
{
|
||||
FileObject fo = createFileObject(repo, request, revId, treeWalk);
|
||||
|
||||
if (fo != null)
|
||||
{
|
||||
files.add(fo);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String[] parts = path.split("/");
|
||||
int current = 0;
|
||||
int limit = parts.length;
|
||||
|
||||
while (treeWalk.next())
|
||||
{
|
||||
String name = treeWalk.getNameString();
|
||||
|
||||
if (current >= limit)
|
||||
{
|
||||
String p = treeWalk.getPathString();
|
||||
|
||||
if (p.split("/").length > limit)
|
||||
{
|
||||
FileObject fo = createFileObject(repo, request, revId, treeWalk);
|
||||
|
||||
if (fo != null)
|
||||
{
|
||||
files.add(fo);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (name.equalsIgnoreCase(parts[current]))
|
||||
{
|
||||
current++;
|
||||
|
||||
if (!request.isRecursive())
|
||||
{
|
||||
if (isRootRequest(request)) {
|
||||
result = createEmtpyRoot();
|
||||
findChildren(result, repo, request, revId, treeWalk);
|
||||
} else {
|
||||
result = findFirstMatch(repo, request, revId, treeWalk);
|
||||
if ( result.isDirectory() ) {
|
||||
treeWalk.enterSubtree();
|
||||
}
|
||||
}
|
||||
findChildren(result, repo, request, revId, treeWalk);
|
||||
}
|
||||
}
|
||||
|
||||
result.setFiles(files);
|
||||
result.setRevision(revId.getName());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -360,6 +305,60 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isRootRequest(BrowseCommandRequest request) {
|
||||
return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath());
|
||||
}
|
||||
|
||||
private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
|
||||
List<FileObject> files = Lists.newArrayList();
|
||||
while (treeWalk.next())
|
||||
{
|
||||
|
||||
FileObject fileObject = createFileObject(repo, request, revId, treeWalk);
|
||||
if (!fileObject.getPath().startsWith(parent.getPath())) {
|
||||
parent.setChildren(files);
|
||||
return fileObject;
|
||||
}
|
||||
|
||||
files.add(fileObject);
|
||||
|
||||
if (request.isRecursive() && fileObject.isDirectory()) {
|
||||
treeWalk.enterSubtree();
|
||||
FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk);
|
||||
if (rc != null) {
|
||||
files.add(rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent.setChildren(files);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
|
||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
|
||||
String[] pathElements = request.getPath().split("/");
|
||||
int currentDepth = 0;
|
||||
int limit = pathElements.length;
|
||||
|
||||
while (treeWalk.next()) {
|
||||
String name = treeWalk.getNameString();
|
||||
|
||||
if (name.equalsIgnoreCase(pathElements[currentDepth])) {
|
||||
currentDepth++;
|
||||
|
||||
if (currentDepth >= limit) {
|
||||
return createFileObject(repo, request, revId, treeWalk);
|
||||
} else {
|
||||
treeWalk.enterSubtree();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundException("file", request.getPath());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String,
|
||||
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repository: Repository
|
||||
repository: Repository,
|
||||
t: string => string
|
||||
}
|
||||
|
||||
class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
const { repository, t } = this.props;
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
@@ -18,11 +20,11 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Clone the repository</h4>
|
||||
<h4>{t("scm-git-plugin.information.clone")}</h4>
|
||||
<pre>
|
||||
<code>git clone {href}</code>
|
||||
</pre>
|
||||
<h4>Create a new repository</h4>
|
||||
<h4>{t("scm-git-plugin.information.create")}</h4>
|
||||
<pre>
|
||||
<code>
|
||||
git init {repository.name}
|
||||
@@ -39,7 +41,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<br />
|
||||
</code>
|
||||
</pre>
|
||||
<h4>Push an existing repository</h4>
|
||||
<h4>{t("scm-git-plugin.information.replace")}</h4>
|
||||
<pre>
|
||||
<code>
|
||||
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.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.GitConstants;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GitBrowseCommand}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitBrowseCommandTest extends AbstractGitCommandTestBase
|
||||
{
|
||||
public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
/**
|
||||
* Test browse command with default branch.
|
||||
*/
|
||||
@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
|
||||
BrowserResult result = 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();
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(4, foList.size());
|
||||
|
||||
assertEquals("a.txt", foList.get(0).getName());
|
||||
assertEquals("b.txt", foList.get(1).getName());
|
||||
assertEquals("c", foList.get(2).getName());
|
||||
assertEquals("f.txt", foList.get(3).getName());
|
||||
|
||||
// set default branch and fetch again
|
||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
||||
result = createCommand().getBrowserResult(new BrowseCommandRequest());
|
||||
assertNotNull(result);
|
||||
|
||||
foList = result.getFiles();
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(2, foList.size());
|
||||
|
||||
assertEquals("a.txt", foList.get(0).getName());
|
||||
assertEquals("c", foList.get(1).getName());
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("a.txt", "b.txt", "c", "f.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowse() throws IOException, RevisionNotFoundException {
|
||||
BrowserResult result =
|
||||
createCommand().getBrowserResult(new BrowseCommandRequest());
|
||||
public void testExplicitDefaultBranch() throws IOException, NotFoundException {
|
||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
||||
|
||||
assertNotNull(result);
|
||||
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
|
||||
assertNotNull(root);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
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;
|
||||
}
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("a.txt", "c");
|
||||
}
|
||||
|
||||
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());
|
||||
assertEquals("a.txt", a.getName());
|
||||
assertEquals("a.txt", a.getPath());
|
||||
assertEquals("added new line for blame", a.getDescription());
|
||||
assertTrue(a.getLength() > 0);
|
||||
checkDate(a.getLastModified());
|
||||
assertNotNull(c);
|
||||
|
||||
assertTrue(c.isDirectory());
|
||||
assertEquals("c", c.getName());
|
||||
assertEquals("c", c.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowseSubDirectory() throws IOException, RevisionNotFoundException {
|
||||
public void testBrowseSubDirectory() throws IOException, NotFoundException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setPath("c");
|
||||
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
assertNotNull(result);
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
assertThat(foList).hasSize(2);
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(2, foList.size());
|
||||
FileObject d = findFile(foList, "d.txt");
|
||||
FileObject e = findFile(foList, "e.txt");
|
||||
|
||||
FileObject d = null;
|
||||
FileObject e = null;
|
||||
|
||||
for (FileObject f : foList)
|
||||
{
|
||||
if ("d.txt".equals(f.getName()))
|
||||
{
|
||||
d = f;
|
||||
}
|
||||
else if ("e.txt".equals(f.getName()))
|
||||
{
|
||||
e = f;
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(d);
|
||||
assertFalse(d.isDirectory());
|
||||
assertEquals("d.txt", d.getName());
|
||||
assertEquals("c/d.txt", d.getPath());
|
||||
assertEquals("added file d and e in folder c", d.getDescription());
|
||||
assertTrue(d.getLength() > 0);
|
||||
checkDate(d.getLastModified());
|
||||
assertNotNull(e);
|
||||
|
||||
assertFalse(e.isDirectory());
|
||||
assertEquals("e.txt", e.getName());
|
||||
assertEquals("c/e.txt", e.getPath());
|
||||
@@ -181,30 +143,35 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecusive() throws IOException, RevisionNotFoundException {
|
||||
public void testRecursive() throws IOException, NotFoundException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
|
||||
request.setRecursive(true);
|
||||
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
FileObject root = createCommand().getBrowserResult(request).getFile();
|
||||
|
||||
assertNotNull(result);
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
assertThat(foList)
|
||||
.extracting("name")
|
||||
.containsExactly("a.txt", "b.txt", "c", "f.txt");
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(5, foList.size());
|
||||
FileObject c = findFile(foList, "c");
|
||||
|
||||
Collection<FileObject> cChildren = c.getChildren();
|
||||
assertThat(cChildren)
|
||||
.extracting("name")
|
||||
.containsExactly("d.txt", "e.txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private GitBrowseCommand createCommand()
|
||||
{
|
||||
private FileObject findFile(Collection<FileObject> foList, String name) {
|
||||
return foList.stream()
|
||||
.filter(f -> name.equals(f.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||
}
|
||||
|
||||
private GitBrowseCommand createCommand() {
|
||||
return new GitBrowseCommand(createContext(), repository);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,9 +707,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -726,7 +726,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.spi.javahg.HgFileviewCommand;
|
||||
|
||||
@@ -45,6 +47,7 @@ import java.io.IOException;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Utilizes the mercurial fileview extension in order to support mercurial repository browsing.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@@ -94,16 +97,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
|
||||
cmd.disableSubRepositoryDetection();
|
||||
}
|
||||
|
||||
BrowserResult result = new BrowserResult();
|
||||
|
||||
result.setFiles(cmd.execute());
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getRevision())) {
|
||||
result.setRevision(request.getRevision());
|
||||
} else {
|
||||
result.setRevision("tip");
|
||||
}
|
||||
|
||||
return result;
|
||||
FileObject file = cmd.execute();
|
||||
return new BrowserResult(MoreObjects.firstNonNull(request.getRevision(), "tip"), file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,35 +50,31 @@ import sonia.scm.repository.SubRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mercurial command to list files of a repository.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class HgFileviewCommand extends AbstractCommand
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
*/
|
||||
public HgFileviewCommand(Repository repository)
|
||||
private boolean disableLastCommit = false;
|
||||
|
||||
private HgFileviewCommand(Repository repository)
|
||||
{
|
||||
super(repository);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Create command for the given repository.
|
||||
*
|
||||
* @param repository repository
|
||||
*
|
||||
* @param repository
|
||||
*
|
||||
* @return
|
||||
* @return fileview command
|
||||
*/
|
||||
public static HgFileviewCommand on(Repository repository)
|
||||
{
|
||||
@@ -86,13 +82,11 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Disable last commit fetching for file objects.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
* @return {@code this}
|
||||
*/
|
||||
public HgFileviewCommand disableLastCommit()
|
||||
{
|
||||
public HgFileviewCommand disableLastCommit() {
|
||||
disableLastCommit = true;
|
||||
cmdAppend("-d");
|
||||
|
||||
@@ -100,132 +94,128 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Disables sub repository detection
|
||||
*
|
||||
*
|
||||
* @return
|
||||
* @return {@code this}
|
||||
*/
|
||||
public HgFileviewCommand disableSubRepositoryDetection()
|
||||
{
|
||||
public HgFileviewCommand disableSubRepositoryDetection() {
|
||||
cmdAppend("-s");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Start file object fetching at the given path.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
* @param path path to start fetching
|
||||
*
|
||||
* @throws IOException
|
||||
* @return {@code this}
|
||||
*/
|
||||
public List<FileObject> execute() throws IOException
|
||||
{
|
||||
cmdAppend("-t");
|
||||
|
||||
List<FileObject> files = Lists.newArrayList();
|
||||
|
||||
HgInputStream stream = launchStream();
|
||||
|
||||
while (stream.peek() != -1)
|
||||
{
|
||||
FileObject file = null;
|
||||
char type = (char) stream.read();
|
||||
|
||||
if (type == 'd')
|
||||
{
|
||||
file = readDirectory(stream);
|
||||
}
|
||||
else if (type == 'f')
|
||||
{
|
||||
file = readFile(stream);
|
||||
}
|
||||
else if (type == 's')
|
||||
{
|
||||
file = readSubRepository(stream);
|
||||
}
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public HgFileviewCommand path(String path)
|
||||
{
|
||||
public HgFileviewCommand path(String path) {
|
||||
cmdAppend("-p", path);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Fetch file objects recursive.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
* @return {@code this}
|
||||
*/
|
||||
public HgFileviewCommand recursive()
|
||||
{
|
||||
public HgFileviewCommand recursive() {
|
||||
cmdAppend("-c");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Use given revision for file view.
|
||||
*
|
||||
* @param revision revision id, hash, tag or branch
|
||||
*
|
||||
* @param revision
|
||||
*
|
||||
* @return
|
||||
* @return {@code this}
|
||||
*/
|
||||
public HgFileviewCommand rev(String revision)
|
||||
{
|
||||
public HgFileviewCommand rev(String revision) {
|
||||
cmdAppend("-r", revision);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Executes the mercurial command and parses the output.
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String getCommandName()
|
||||
{
|
||||
return HgFileviewExtension.NAME;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param stream
|
||||
*
|
||||
* @return
|
||||
* @return file object
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private FileObject readDirectory(HgInputStream stream) throws IOException
|
||||
public FileObject execute() throws IOException
|
||||
{
|
||||
cmdAppend("-t");
|
||||
|
||||
Deque<FileObject> stack = new LinkedList<>();
|
||||
|
||||
HgInputStream stream = launchStream();
|
||||
|
||||
FileObject last = null;
|
||||
while (stream.peek() != -1) {
|
||||
FileObject file = read(stream);
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
FileObject current = stack.peek();
|
||||
if (isParent(current, file)) {
|
||||
current.addChild(file);
|
||||
break;
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if (file.isDirectory()) {
|
||||
stack.push(file);
|
||||
}
|
||||
last = file;
|
||||
}
|
||||
|
||||
if (stack.isEmpty()) {
|
||||
// if the stack is empty, the requested path is probably a file
|
||||
return last;
|
||||
} else {
|
||||
// if the stack is not empty, the requested path is a directory
|
||||
return stack.getLast();
|
||||
}
|
||||
}
|
||||
|
||||
private FileObject read(HgInputStream stream) throws IOException {
|
||||
char type = (char) stream.read();
|
||||
|
||||
FileObject file;
|
||||
switch (type) {
|
||||
case 'd':
|
||||
file = readDirectory(stream);
|
||||
break;
|
||||
case 'f':
|
||||
file = readFile(stream);
|
||||
break;
|
||||
case 's':
|
||||
file = readSubRepository(stream);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("unknown file object type: " + type);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private boolean isParent(FileObject parent, FileObject child) {
|
||||
String parentPath = parent.getPath();
|
||||
if (parentPath.equals("")) {
|
||||
return true;
|
||||
}
|
||||
return child.getParentPath().equals(parentPath);
|
||||
}
|
||||
|
||||
private FileObject readDirectory(HgInputStream stream) throws IOException {
|
||||
FileObject directory = new FileObject();
|
||||
String path = removeTrailingSlash(stream.textUpTo('\0'));
|
||||
|
||||
@@ -236,18 +226,7 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param stream
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private FileObject readFile(HgInputStream stream) throws IOException
|
||||
{
|
||||
private FileObject readFile(HgInputStream stream) throws IOException {
|
||||
FileObject file = new FileObject();
|
||||
String path = removeTrailingSlash(stream.textUpTo('\n'));
|
||||
|
||||
@@ -259,8 +238,7 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
DateTime timestamp = stream.dateTimeUpTo(' ');
|
||||
String description = stream.textUpTo('\0');
|
||||
|
||||
if (!disableLastCommit)
|
||||
{
|
||||
if (!disableLastCommit) {
|
||||
file.setLastModified(timestamp.getDate().getTime());
|
||||
file.setDescription(description);
|
||||
}
|
||||
@@ -268,18 +246,7 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param stream
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private FileObject readSubRepository(HgInputStream stream) throws IOException
|
||||
{
|
||||
private FileObject readSubRepository(HgInputStream stream) throws IOException {
|
||||
FileObject directory = new FileObject();
|
||||
String path = removeTrailingSlash(stream.textUpTo('\n'));
|
||||
|
||||
@@ -292,8 +259,7 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
|
||||
SubRepository subRepository = new SubRepository(url);
|
||||
|
||||
if (!Strings.isNullOrEmpty(revision))
|
||||
{
|
||||
if (!Strings.isNullOrEmpty(revision)) {
|
||||
subRepository.setRevision(revision);
|
||||
}
|
||||
|
||||
@@ -302,48 +268,33 @@ public class HgFileviewCommand extends AbstractCommand
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String removeTrailingSlash(String path)
|
||||
{
|
||||
if (path.endsWith("/"))
|
||||
{
|
||||
private String removeTrailingSlash(String path) {
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substring(0, path.length() - 1);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String getNameFromPath(String path)
|
||||
{
|
||||
private String getNameFromPath(String path) {
|
||||
int index = path.lastIndexOf('/');
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
if (index > 0) {
|
||||
path = path.substring(index + 1);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
/**
|
||||
* Returns the name of the mercurial command.
|
||||
*
|
||||
* @return command name
|
||||
*/
|
||||
@Override
|
||||
public String getCommandName()
|
||||
{
|
||||
return HgFileviewExtension.NAME;
|
||||
}
|
||||
|
||||
/** Field description */
|
||||
private boolean disableLastCommit = false;
|
||||
}
|
||||
|
||||
@@ -2,26 +2,28 @@
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repository: Repository
|
||||
repository: Repository,
|
||||
t: string => string
|
||||
}
|
||||
|
||||
class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
const { repository, t } = this.props;
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h4>Clone the repository</h4>
|
||||
<h4>{t("scm-hg-plugin.information.clone")}</h4>
|
||||
<pre>
|
||||
<code>hg clone {href}</code>
|
||||
</pre>
|
||||
<h4>Create a new repository</h4>
|
||||
<h4>{t("scm-hg-plugin.information.create")}</h4>
|
||||
<pre>
|
||||
<code>
|
||||
hg init {repository.name}
|
||||
@@ -41,7 +43,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<br />
|
||||
</code>
|
||||
</pre>
|
||||
<h4>Push an existing repository</h4>
|
||||
<h4>{t("scm-hg-plugin.information.replace")}</h4>
|
||||
<pre>
|
||||
<code>
|
||||
# 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.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from mercurial import cmdutil,util
|
||||
|
||||
cmdtable = {}
|
||||
command = cmdutil.command(cmdtable)
|
||||
|
||||
FILE_MARKER = '<files>'
|
||||
|
||||
class File_Collector:
|
||||
|
||||
def __init__(self, recursive = False):
|
||||
self.recursive = recursive
|
||||
self.structure = defaultdict(dict, ((FILE_MARKER, []),))
|
||||
|
||||
def collect(self, paths, path = "", dir_only = False):
|
||||
for p in paths:
|
||||
if p.startswith(path):
|
||||
self.attach(self.extract_name_without_parent(path, p), self.structure, dir_only)
|
||||
|
||||
def attach(self, branch, trunk, dir_only = False):
|
||||
parts = branch.split('/', 1)
|
||||
if len(parts) == 1: # branch is a file
|
||||
if dir_only:
|
||||
trunk[parts[0]] = defaultdict(dict, ((FILE_MARKER, []),))
|
||||
else:
|
||||
trunk[FILE_MARKER].append(parts[0])
|
||||
else:
|
||||
node, others = parts
|
||||
if node not in trunk:
|
||||
trunk[node] = defaultdict(dict, ((FILE_MARKER, []),))
|
||||
if self.recursive:
|
||||
self.attach(others, trunk[node], dir_only)
|
||||
|
||||
def extract_name_without_parent(self, parent, name_with_parent):
|
||||
if len(parent) > 0:
|
||||
name_without_parent = name_with_parent[len(parent):]
|
||||
if name_without_parent.startswith("/"):
|
||||
name_without_parent = name_without_parent[1:]
|
||||
return name_without_parent
|
||||
return name_with_parent
|
||||
|
||||
class File_Object:
|
||||
def __init__(self, directory, path):
|
||||
self.directory = directory
|
||||
self.path = path
|
||||
self.children = []
|
||||
self.sub_repository = None
|
||||
|
||||
def get_name(self):
|
||||
parts = self.path.split("/")
|
||||
return parts[len(parts) - 1]
|
||||
|
||||
def get_parent(self):
|
||||
idx = self.path.rfind("/")
|
||||
if idx > 0:
|
||||
return self.path[0:idx]
|
||||
return ""
|
||||
|
||||
def add_child(self, child):
|
||||
self.children.append(child)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.children[key]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.children)
|
||||
|
||||
def __repr__(self):
|
||||
result = self.path
|
||||
if self.directory:
|
||||
result += "/"
|
||||
return result
|
||||
|
||||
class File_Walker:
|
||||
|
||||
def __init__(self, sub_repositories, visitor):
|
||||
self.visitor = visitor
|
||||
self.sub_repositories = sub_repositories
|
||||
|
||||
def create_file(self, path):
|
||||
return File_Object(False, path)
|
||||
|
||||
def create_directory(self, path):
|
||||
directory = File_Object(True, path)
|
||||
if path in self.sub_repositories:
|
||||
directory.sub_repository = self.sub_repositories[path]
|
||||
return directory
|
||||
|
||||
def visit_file(self, path):
|
||||
file = self.create_file(path)
|
||||
self.visit(file)
|
||||
|
||||
def visit_directory(self, path):
|
||||
file = self.create_directory(path)
|
||||
self.visit(file)
|
||||
|
||||
def visit(self, file):
|
||||
self.visitor.visit(file)
|
||||
|
||||
def create_path(self, parent, path):
|
||||
if len(parent) > 0:
|
||||
return parent + "/" + path
|
||||
return path
|
||||
|
||||
def walk(self, structure, parent = ""):
|
||||
for key, value in structure.iteritems():
|
||||
if key == FILE_MARKER:
|
||||
if value:
|
||||
for v in value:
|
||||
self.visit_file(self.create_path(parent, v))
|
||||
else:
|
||||
self.visit_directory(self.create_path(parent, key))
|
||||
if isinstance(value, dict):
|
||||
self.walk(value, self.create_path(parent, key))
|
||||
else:
|
||||
self.visit_directory(self.create_path(parent, value))
|
||||
|
||||
class SubRepository:
|
||||
url = None
|
||||
revision = None
|
||||
|
||||
def removeTrailingSlash(path):
|
||||
if path.endswith('/'):
|
||||
path = path[0:-1]
|
||||
return path
|
||||
|
||||
def appendTrailingSlash(path):
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
return path
|
||||
|
||||
def collectFiles(revCtx, path, files, directories, recursive):
|
||||
length = 0
|
||||
paths = []
|
||||
mf = revCtx.manifest()
|
||||
if path is "":
|
||||
length = 1
|
||||
for f in mf:
|
||||
paths.append(f)
|
||||
else:
|
||||
length = len(path.split('/')) + 1
|
||||
directory = path
|
||||
if not directory.endswith('/'):
|
||||
directory += '/'
|
||||
|
||||
for f in mf:
|
||||
if f.startswith(directory):
|
||||
paths.append(f)
|
||||
|
||||
if not recursive:
|
||||
for p in paths:
|
||||
parts = p.split('/')
|
||||
depth = len(parts)
|
||||
if depth is length:
|
||||
file = revCtx[p]
|
||||
files.append(file)
|
||||
elif depth > length:
|
||||
dirpath = ''
|
||||
for i in range(0, length):
|
||||
dirpath += parts[i] + '/'
|
||||
if not dirpath in directories:
|
||||
directories.append(dirpath)
|
||||
else:
|
||||
for p in paths:
|
||||
files.append(revCtx[p])
|
||||
|
||||
def createSubRepositoryMap(revCtx):
|
||||
def collect_sub_repositories(revCtx):
|
||||
subrepos = {}
|
||||
try:
|
||||
hgsub = revCtx.filectx('.hgsub').data().split('\n')
|
||||
@@ -112,29 +180,74 @@ def createSubRepositoryMap(revCtx):
|
||||
|
||||
return subrepos
|
||||
|
||||
def printSubRepository(ui, path, subrepository, transport):
|
||||
format = '%s %s %s\n'
|
||||
if transport:
|
||||
format = 's%s\n%s %s\0'
|
||||
ui.write( format % (appendTrailingSlash(path), subrepository.revision, subrepository.url))
|
||||
class File_Printer:
|
||||
|
||||
def printDirectory(ui, path, transport):
|
||||
format = '%s\n'
|
||||
if transport:
|
||||
format = 'd%s\0'
|
||||
ui.write( format % path)
|
||||
def __init__(self, ui, repo, revCtx, disableLastCommit, transport):
|
||||
self.ui = ui
|
||||
self.repo = repo
|
||||
self.revCtx = revCtx
|
||||
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'
|
||||
description = 'n/a'
|
||||
if not disableLastCommit:
|
||||
linkrev = repo[file.linkrev()]
|
||||
if not self.disableLastCommit:
|
||||
linkrev = self.repo[file.linkrev()]
|
||||
date = '%d %d' % util.parsedate(linkrev.date())
|
||||
description = linkrev.description()
|
||||
format = '%s %i %s %s\n'
|
||||
if transport:
|
||||
if self.transport:
|
||||
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', [
|
||||
('r', 'revision', 'tip', 'revision to print'),
|
||||
@@ -145,23 +258,12 @@ def printFile(ui, repo, file, disableLastCommit, transport):
|
||||
('t', 'transport', False, 'format the output for command server'),
|
||||
])
|
||||
def fileview(ui, repo, **opts):
|
||||
files = []
|
||||
directories = []
|
||||
revision = opts['revision']
|
||||
if revision == None:
|
||||
revision = 'tip'
|
||||
revCtx = repo[revision]
|
||||
path = opts['path']
|
||||
if path.endswith('/'):
|
||||
path = path[0:-1]
|
||||
transport = opts['transport']
|
||||
collectFiles(revCtx, path, files, directories, opts['recursive'])
|
||||
if not opts['disableSubRepositoryDetection']:
|
||||
subRepositories = createSubRepositoryMap(revCtx)
|
||||
for k, v in subRepositories.iteritems():
|
||||
if k.startswith(path):
|
||||
printSubRepository(ui, k, v, transport)
|
||||
for d in directories:
|
||||
printDirectory(ui, d, transport)
|
||||
for f in files:
|
||||
printFile(ui, repo, f, opts['disableLastCommit'], transport)
|
||||
revCtx = repo[opts["revision"]]
|
||||
subrepos = {}
|
||||
if not opts["disableSubRepositoryDetection"]:
|
||||
subrepos = collect_sub_repositories(revCtx)
|
||||
printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"])
|
||||
viewer = File_Viewer(revCtx, printer)
|
||||
viewer.recursive = opts["recursive"]
|
||||
viewer.sub_repositories = subrepos
|
||||
viewer.view(opts["path"])
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
from fileview import File_Viewer, SubRepository
|
||||
import unittest
|
||||
|
||||
class DummyRevContext():
|
||||
|
||||
def __init__(self, mf):
|
||||
self.mf = mf
|
||||
|
||||
def manifest(self):
|
||||
return self.mf
|
||||
|
||||
class File_Object_Collector():
|
||||
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
|
||||
def __getitem__(self, key):
|
||||
if len(self.stack) == 0 and key == 0:
|
||||
return self.last
|
||||
return self.stack[key]
|
||||
|
||||
def visit(self, file):
|
||||
while len(self.stack) > 0:
|
||||
current = self.stack[-1]
|
||||
if file.get_parent() == current.path:
|
||||
current.add_child(file)
|
||||
break
|
||||
else:
|
||||
self.stack.pop()
|
||||
if file.directory:
|
||||
self.stack.append(file)
|
||||
self.last = file
|
||||
|
||||
|
||||
class Test_File_Viewer(unittest.TestCase):
|
||||
|
||||
def test_single_file(self):
|
||||
root = self.collect(["a.txt", "b.txt"], "a.txt")
|
||||
self.assertFile(root, "a.txt")
|
||||
|
||||
def test_simple(self):
|
||||
root = self.collect(["a.txt", "b.txt"])
|
||||
self.assertFile(root[0], "a.txt")
|
||||
self.assertFile(root[1], "b.txt")
|
||||
|
||||
def test_recursive(self):
|
||||
root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "", True)
|
||||
self.assertChildren(root, ["a", "b", "f.txt", "c"])
|
||||
c = root[3]
|
||||
self.assertDirectory(c, "c")
|
||||
self.assertChildren(c, ["c/d.txt", "c/e.txt", "c/g"])
|
||||
g = c[2]
|
||||
self.assertDirectory(g, "c/g")
|
||||
self.assertChildren(g, ["c/g/h.txt"])
|
||||
|
||||
def test_recursive_with_path(self):
|
||||
root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c", True)
|
||||
self.assertDirectory(root, "c")
|
||||
self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/g"])
|
||||
g = root[2]
|
||||
self.assertDirectory(g, "c/g")
|
||||
self.assertChildren(g, ["c/g/h.txt"])
|
||||
|
||||
def test_recursive_with_deep_path(self):
|
||||
root = self.collect(["a", "b", "c/d.txt", "c/e.txt", "f.txt", "c/g/h.txt"], "c/g", True)
|
||||
self.assertDirectory(root, "c/g")
|
||||
self.assertChildren(root, ["c/g/h.txt"])
|
||||
|
||||
def test_non_recursive(self):
|
||||
root = self.collect(["a.txt", "b.txt", "c/d.txt", "c/e.txt", "c/f/g.txt"])
|
||||
self.assertDirectory(root, "")
|
||||
self.assertChildren(root, ["a.txt", "b.txt", "c"])
|
||||
c = root[2]
|
||||
self.assertEmptyDirectory(c, "c")
|
||||
|
||||
def test_non_recursive_with_path(self):
|
||||
root = self.collect(["a.txt", "b.txt", "c/d.txt", "c/e.txt", "c/f/g.txt"], "c")
|
||||
self.assertDirectory(root, "c")
|
||||
self.assertChildren(root, ["c/d.txt", "c/e.txt", "c/f"])
|
||||
f = root[2]
|
||||
self.assertEmptyDirectory(f, "c/f")
|
||||
|
||||
def test_non_recursive_with_path_with_ending_slash(self):
|
||||
root = self.collect(["c/d.txt"], "c/")
|
||||
self.assertDirectory(root, "c")
|
||||
self.assertFile(root[0], "c/d.txt")
|
||||
|
||||
def test_with_sub_directory(self):
|
||||
revCtx = DummyRevContext(["a.txt", "b/c.txt"])
|
||||
collector = File_Object_Collector()
|
||||
viewer = File_Viewer(revCtx, collector)
|
||||
sub_repositories = {}
|
||||
sub_repositories["d"] = SubRepository()
|
||||
sub_repositories["d"].url = "d"
|
||||
sub_repositories["d"].revision = "42"
|
||||
viewer.sub_repositories = sub_repositories
|
||||
viewer.view()
|
||||
|
||||
d = collector[0][2]
|
||||
self.assertDirectory(d, "d")
|
||||
|
||||
|
||||
def collect(self, paths, path = "", recursive = False):
|
||||
revCtx = DummyRevContext(paths)
|
||||
collector = File_Object_Collector()
|
||||
|
||||
viewer = File_Viewer(revCtx, collector)
|
||||
viewer.recursive = recursive
|
||||
viewer.view(path)
|
||||
|
||||
return collector[0]
|
||||
|
||||
def assertChildren(self, parent, expectedPaths):
|
||||
self.assertEqual(len(parent), len(expectedPaths))
|
||||
for idx,item in enumerate(parent.children):
|
||||
self.assertEqual(item.path, expectedPaths[idx])
|
||||
|
||||
def assertFile(self, file, expectedPath):
|
||||
self.assertEquals(file.path, expectedPath)
|
||||
self.assertFalse(file.directory)
|
||||
|
||||
def assertDirectory(self, file, expectedPath):
|
||||
self.assertEquals(file.path, expectedPath)
|
||||
self.assertTrue(file.directory)
|
||||
|
||||
def assertEmptyDirectory(self, file, expectedPath):
|
||||
self.assertDirectory(file, expectedPath)
|
||||
self.assertTrue(len(file.children) == 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -33,14 +33,12 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -48,18 +46,25 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
{
|
||||
public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void testBrowseWithFilePath() throws IOException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
request.setPath("a.txt");
|
||||
FileObject file = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
|
||||
assertEquals("a.txt", file.getName());
|
||||
assertFalse(file.isDirectory());
|
||||
assertTrue(file.getChildren().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowse() throws IOException {
|
||||
List<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
|
||||
@@ -85,7 +90,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
FileObject c = result.getFile();
|
||||
assertEquals("c", c.getName());
|
||||
Collection<FileObject> foList = c.getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
@@ -128,7 +135,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
request.setDisableLastCommit(true);
|
||||
|
||||
List<FileObject> foList = getRootFromTip(request);
|
||||
Collection<FileObject> foList = getRootFromTip(request);
|
||||
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
|
||||
@@ -147,11 +154,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
FileObject root = result.getFile();
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(5, foList.size());
|
||||
assertEquals(4, foList.size());
|
||||
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
assertTrue(c.isDirectory());
|
||||
assertEquals(2, c.getChildren().size());
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
@@ -165,32 +177,22 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private FileObject getFileObject(List<FileObject> foList, String name)
|
||||
private FileObject getFileObject(Collection<FileObject> foList, String name)
|
||||
{
|
||||
FileObject a = null;
|
||||
|
||||
for (FileObject f : foList)
|
||||
{
|
||||
if (name.equals(f.getName()))
|
||||
{
|
||||
a = f;
|
||||
|
||||
break;
|
||||
}
|
||||
return foList.stream()
|
||||
.filter(f -> name.equals(f.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||
}
|
||||
|
||||
assertNotNull(a);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
private List<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
|
||||
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
|
||||
BrowserResult result = new HgBrowseCommand(cmdContext,
|
||||
repository).getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
FileObject root = result.getFile();
|
||||
Collection<FileObject> foList = root.getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
|
||||
@@ -641,9 +641,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -660,7 +660,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -79,11 +80,11 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||
String path = request.getPath();
|
||||
String path = Strings.nullToEmpty(request.getPath());
|
||||
long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision());
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("browser repository {} in path {} at revision {}", repository.getName(), path, revisionNumber);
|
||||
logger.debug("browser repository {} in path \"{}\" at revision {}", repository.getName(), path, revisionNumber);
|
||||
}
|
||||
|
||||
BrowserResult result = null;
|
||||
@@ -91,34 +92,21 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
||||
try
|
||||
{
|
||||
SVNRepository svnRepository = open();
|
||||
Collection<SVNDirEntry> entries =
|
||||
svnRepository.getDir(Util.nonNull(path), revisionNumber, null,
|
||||
(Collection) null);
|
||||
List<FileObject> children = Lists.newArrayList();
|
||||
String basePath = createBasePath(path);
|
||||
|
||||
if (request.isRecursive())
|
||||
{
|
||||
browseRecursive(svnRepository, revisionNumber, request, children,
|
||||
entries, basePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (SVNDirEntry entry : entries)
|
||||
{
|
||||
children.add(createFileObject(request, svnRepository, revisionNumber,
|
||||
entry, basePath));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (revisionNumber == -1) {
|
||||
revisionNumber = svnRepository.getLatestRevision();
|
||||
}
|
||||
|
||||
result = new BrowserResult();
|
||||
result.setRevision(String.valueOf(revisionNumber));
|
||||
result.setFiles(children);
|
||||
SVNDirEntry rootEntry = svnRepository.info(path, revisionNumber);
|
||||
FileObject root = createFileObject(request, svnRepository, revisionNumber, rootEntry, path);
|
||||
root.setPath(path);
|
||||
|
||||
if (root.isDirectory()) {
|
||||
traverse(svnRepository, revisionNumber, request, root, createBasePath(path));
|
||||
}
|
||||
|
||||
|
||||
result = new BrowserResult(String.valueOf(revisionNumber), root);
|
||||
}
|
||||
catch (SVNException ex)
|
||||
{
|
||||
@@ -130,52 +118,24 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param svnRepository
|
||||
* @param revisionNumber
|
||||
* @param request
|
||||
* @param children
|
||||
* @param entries
|
||||
* @param basePath
|
||||
*
|
||||
* @throws SVNException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void browseRecursive(SVNRepository svnRepository,
|
||||
long revisionNumber, BrowseCommandRequest request,
|
||||
List<FileObject> children, Collection<SVNDirEntry> entries, String basePath)
|
||||
private void traverse(SVNRepository svnRepository, long revisionNumber, BrowseCommandRequest request,
|
||||
FileObject parent, String basePath)
|
||||
throws SVNException
|
||||
{
|
||||
Collection<SVNDirEntry> entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null);
|
||||
for (SVNDirEntry entry : entries)
|
||||
{
|
||||
FileObject fo = createFileObject(request, svnRepository, revisionNumber,
|
||||
entry, basePath);
|
||||
FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath);
|
||||
|
||||
children.add(fo);
|
||||
parent.addChild(child);
|
||||
|
||||
if (fo.isDirectory())
|
||||
{
|
||||
Collection<SVNDirEntry> subEntries =
|
||||
svnRepository.getDir(Util.nonNull(fo.getPath()), revisionNumber,
|
||||
null, (Collection) null);
|
||||
|
||||
browseRecursive(svnRepository, revisionNumber, request, children,
|
||||
subEntries, createBasePath(fo.getPath()));
|
||||
if (child.isDirectory() && request.isRecursive()) {
|
||||
traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String createBasePath(String path)
|
||||
{
|
||||
String basePath = Util.EMPTY_STRING;
|
||||
@@ -193,20 +153,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
||||
return basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param repository
|
||||
* @param revision
|
||||
* @param entry
|
||||
* @param path
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private FileObject createFileObject(BrowseCommandRequest request,
|
||||
SVNRepository repository, long revision, SVNDirEntry entry, String path)
|
||||
{
|
||||
@@ -237,15 +183,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
|
||||
return fileObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
* @param revision
|
||||
* @param entry
|
||||
* @param fileObject
|
||||
*/
|
||||
private void fetchExternalsProperty(SVNRepository repository, long revision,
|
||||
SVNDirEntry entry, FileObject fileObject)
|
||||
{
|
||||
|
||||
@@ -2,22 +2,24 @@
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repository: Repository
|
||||
repository: Repository,
|
||||
t: string => string
|
||||
}
|
||||
|
||||
class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
const { repository, t } = this.props;
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h4>Checkout the repository</h4>
|
||||
<h4>{t("scm-svn-plugin.information.checkout")}</h4>
|
||||
<pre>
|
||||
<code>svn checkout {href}</code>
|
||||
</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;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.BrowserResult;
|
||||
import sonia.scm.repository.FileObject;
|
||||
import sonia.scm.repository.RevisionNotFoundException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -49,8 +47,6 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -58,9 +54,19 @@ import static org.junit.Assert.assertTrue;
|
||||
public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
{
|
||||
|
||||
@Test
|
||||
public void testBrowseWithFilePath() throws RevisionNotFoundException {
|
||||
BrowseCommandRequest request = new BrowseCommandRequest();
|
||||
request.setPath("a.txt");
|
||||
FileObject file = createCommand().getBrowserResult(request).getFile();
|
||||
assertEquals("a.txt", file.getName());
|
||||
assertFalse(file.isDirectory());
|
||||
assertTrue(file.getChildren().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBrowse() throws RevisionNotFoundException {
|
||||
List<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
|
||||
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
@@ -92,7 +98,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
@@ -135,7 +141,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
request.setDisableLastCommit(true);
|
||||
|
||||
List<FileObject> foList = getRootFromTip(request);
|
||||
Collection<FileObject> foList = getRootFromTip(request);
|
||||
|
||||
FileObject a = getFileObject(foList, "a.txt");
|
||||
|
||||
@@ -151,15 +157,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
assertEquals(4, foList.size());
|
||||
assertEquals(2, foList.size());
|
||||
|
||||
for ( FileObject fo : foList ){
|
||||
System.out.println(fo);
|
||||
}
|
||||
FileObject c = getFileObject(foList, "c");
|
||||
assertEquals("c", c.getName());
|
||||
assertTrue(c.isDirectory());
|
||||
assertEquals(2, c.getChildren().size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,31 +191,20 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private FileObject getFileObject(List<FileObject> foList, String name)
|
||||
private FileObject getFileObject(Collection<FileObject> foList, String name)
|
||||
{
|
||||
FileObject a = null;
|
||||
|
||||
for (FileObject f : foList)
|
||||
{
|
||||
if (name.equals(f.getName()))
|
||||
{
|
||||
a = f;
|
||||
|
||||
break;
|
||||
}
|
||||
return foList.stream()
|
||||
.filter(f -> name.equals(f.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("file " + name + " not found"));
|
||||
}
|
||||
|
||||
assertNotNull(a);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
private List<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||
private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
|
||||
BrowserResult result = createCommand().getBrowserResult(request);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
List<FileObject> foList = result.getFiles();
|
||||
Collection<FileObject> foList = result.getFile().getChildren();
|
||||
|
||||
assertNotNull(foList);
|
||||
assertFalse(foList.isEmpty());
|
||||
|
||||
@@ -641,9 +641,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -660,7 +660,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -196,7 +196,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> {
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void testModifyNotExisting() throws NotFoundException, ConcurrentModificationException {
|
||||
public void testModifyNotExisting() {
|
||||
manager.modify(UserTestData.createZaphod());
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> {
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void testRefreshNotFound() throws NotFoundException {
|
||||
public void testRefreshNotFound(){
|
||||
manager.refresh(UserTestData.createDent());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
"eslint-fix": "eslint src --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15",
|
||||
"@scm-manager/ui-bundler": "^0.0.17",
|
||||
"create-index": "^2.3.0",
|
||||
"enzyme": "^3.5.0",
|
||||
"enzyme-adapter-react-16": "^1.3.1",
|
||||
"flow-bin": "^0.79.1",
|
||||
"flow-typed": "^2.5.1",
|
||||
"jest": "^23.5.0",
|
||||
"raf": "^3.4.0"
|
||||
"raf": "^3.4.0",
|
||||
"react-router-enzyme-context": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-i18next": "^7.11.0",
|
||||
"react-jss": "^8.6.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
|
||||
@@ -9,9 +9,18 @@ type Props = {
|
||||
};
|
||||
|
||||
class Image extends React.Component<Props> {
|
||||
|
||||
createImageSrc = () => {
|
||||
const { src } = this.props;
|
||||
if (src.startsWith("http")) {
|
||||
return src;
|
||||
}
|
||||
return withContextPath(src);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { src, alt, className } = this.props;
|
||||
return <img className={className} src={withContextPath(src)} alt={alt} />;
|
||||
const { alt, className } = this.props;
|
||||
return <img className={className} src={this.createImageSrc()} alt={alt} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
133
scm-ui-components/packages/ui-components/src/LinkPaginator.js
Normal file
133
scm-ui-components/packages/ui-components/src/LinkPaginator.js
Normal file
@@ -0,0 +1,133 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import type {PagedCollection} from "@scm-manager/ui-types";
|
||||
import {Button} from "./buttons";
|
||||
|
||||
type Props = {
|
||||
collection: PagedCollection,
|
||||
page: number,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class LinkPaginator extends React.Component<Props> {
|
||||
|
||||
renderFirstButton() {
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-link"}
|
||||
label={"1"}
|
||||
disabled={false}
|
||||
link={"1"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderPreviousButton(label?: string) {
|
||||
const { page } = this.props;
|
||||
const previousPage = page - 1;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-previous"}
|
||||
label={label ? label : previousPage.toString()}
|
||||
disabled={!this.hasLink("prev")}
|
||||
link={`${previousPage}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
hasLink(name: string) {
|
||||
const { collection } = this.props;
|
||||
return collection._links[name];
|
||||
}
|
||||
|
||||
renderNextButton(label?: string) {
|
||||
const { page } = this.props;
|
||||
const nextPage = page + 1;
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-next"}
|
||||
label={label ? label : nextPage.toString()}
|
||||
disabled={!this.hasLink("next")}
|
||||
link={`${nextPage}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLastButton() {
|
||||
const { collection } = this.props;
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-link"}
|
||||
label={`${collection.pageTotal}`}
|
||||
disabled={false}
|
||||
link={`${collection.pageTotal}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
separator() {
|
||||
return <span className="pagination-ellipsis">…</span>;
|
||||
}
|
||||
|
||||
currentPage(page: number) {
|
||||
return (
|
||||
<Button
|
||||
className="pagination-link is-current"
|
||||
label={page}
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
pageLinks() {
|
||||
const { collection } = this.props;
|
||||
|
||||
const links = [];
|
||||
const page = collection.page + 1;
|
||||
const pageTotal = collection.pageTotal;
|
||||
if (page > 1) {
|
||||
links.push(this.renderFirstButton());
|
||||
}
|
||||
if (page > 3) {
|
||||
links.push(this.separator());
|
||||
}
|
||||
if (page > 2) {
|
||||
links.push(this.renderPreviousButton());
|
||||
}
|
||||
|
||||
links.push(this.currentPage(page));
|
||||
|
||||
if (page + 1 < pageTotal) {
|
||||
links.push(this.renderNextButton());
|
||||
}
|
||||
if (page + 2 < pageTotal)
|
||||
//if there exists pages between next and last
|
||||
links.push(this.separator());
|
||||
if (page < pageTotal) {
|
||||
links.push(this.renderLastButton());
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<nav className="pagination is-centered" aria-label="pagination">
|
||||
{this.renderPreviousButton(t("paginator.previous"))}
|
||||
<ul className="pagination-list">
|
||||
{this.pageLinks().map((link, index) => {
|
||||
return <li key={index}>{link}</li>;
|
||||
})}
|
||||
</ul>
|
||||
{this.renderNextButton(t("paginator.next"))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(LinkPaginator);
|
||||
@@ -18,8 +18,10 @@ class Paginator extends React.Component<Props> {
|
||||
createAction = (linkType: string) => () => {
|
||||
const { collection, onPageChange } = this.props;
|
||||
if (onPageChange) {
|
||||
const link = collection._links[linkType].href;
|
||||
onPageChange(link);
|
||||
const link = collection._links[linkType];
|
||||
if (link && link.href) {
|
||||
onPageChange(link.href);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,13 @@ import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import "./tests/enzyme";
|
||||
import "./tests/i18n";
|
||||
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import Paginator from "./Paginator";
|
||||
|
||||
describe("paginator rendering tests", () => {
|
||||
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
const dummyLink = {
|
||||
href: "https://dummy"
|
||||
};
|
||||
@@ -18,7 +21,10 @@ describe("paginator rendering tests", () => {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(7);
|
||||
for (let button of buttons) {
|
||||
@@ -37,7 +43,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(5);
|
||||
|
||||
@@ -73,7 +82,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(6);
|
||||
|
||||
@@ -112,7 +124,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(5);
|
||||
|
||||
@@ -148,7 +163,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(6);
|
||||
|
||||
@@ -189,7 +207,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(7);
|
||||
|
||||
@@ -244,7 +265,8 @@ describe("paginator rendering tests", () => {
|
||||
};
|
||||
|
||||
const paginator = mount(
|
||||
<Paginator collection={collection} onPageChange={callMe} />
|
||||
<Paginator collection={collection} onPageChange={callMe} />,
|
||||
options.get()
|
||||
);
|
||||
paginator.find("Button.pagination-previous").simulate("click");
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Link } from "react-router-dom";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
export type ButtonProps = {
|
||||
label: string,
|
||||
@@ -16,7 +16,10 @@ export type ButtonProps = {
|
||||
|
||||
type Props = ButtonProps & {
|
||||
type: string,
|
||||
color: string
|
||||
color: string,
|
||||
|
||||
// context prop
|
||||
history: any
|
||||
};
|
||||
|
||||
class Button extends React.Component<Props> {
|
||||
@@ -25,14 +28,22 @@ class Button extends React.Component<Props> {
|
||||
color: "default"
|
||||
};
|
||||
|
||||
renderButton = () => {
|
||||
onClick = (event: Event) => {
|
||||
const { action, link, history } = this.props;
|
||||
if (action) {
|
||||
action(event);
|
||||
} else if (link) {
|
||||
history.push(link);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
loading,
|
||||
disabled,
|
||||
type,
|
||||
color,
|
||||
action,
|
||||
fullWidth,
|
||||
className
|
||||
} = this.props;
|
||||
@@ -42,7 +53,7 @@ class Button extends React.Component<Props> {
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={action ? action : (event: Event) => {}}
|
||||
onClick={this.onClick}
|
||||
className={classNames(
|
||||
"button",
|
||||
"is-" + color,
|
||||
@@ -56,14 +67,6 @@ class Button extends React.Component<Props> {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { link } = this.props;
|
||||
if (link) {
|
||||
return <Link to={link}>{this.renderButton()}</Link>;
|
||||
} else {
|
||||
return this.renderButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
export default withRouter(Button);
|
||||
|
||||
@@ -15,9 +15,11 @@ export { default as Logo } from "./Logo.js";
|
||||
export { default as MailLink } from "./MailLink.js";
|
||||
export { default as Notification } from "./Notification.js";
|
||||
export { default as Paginator } from "./Paginator.js";
|
||||
export { default as LinkPaginator } from "./LinkPaginator.js";
|
||||
export { default as ProtectedRoute } from "./ProtectedRoute.js";
|
||||
export { default as Help } from "./Help.js";
|
||||
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
|
||||
export { getPageFromMatch } from "./urls";
|
||||
|
||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Route, Link } from "react-router-dom";
|
||||
type Props = {
|
||||
to: string,
|
||||
label: string,
|
||||
activeOnlyWhenExact?: boolean
|
||||
activeOnlyWhenExact?: boolean,
|
||||
activeWhenMatch?: (route: any) => boolean
|
||||
};
|
||||
|
||||
class NavLink extends React.Component<Props> {
|
||||
@@ -15,11 +16,17 @@ class NavLink extends React.Component<Props> {
|
||||
activeOnlyWhenExact: true
|
||||
};
|
||||
|
||||
|
||||
isActive(route: any) {
|
||||
const { activeWhenMatch } = this.props;
|
||||
return route.match || (activeWhenMatch && activeWhenMatch(route));
|
||||
}
|
||||
|
||||
renderLink = (route: any) => {
|
||||
const { to, label } = this.props;
|
||||
return (
|
||||
<li>
|
||||
<Link className={route.match ? "is-active" : ""} to={to}>
|
||||
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -4,38 +4,58 @@ import { translate } from "react-i18next";
|
||||
import PrimaryNavigationLink from "./PrimaryNavigationLink";
|
||||
|
||||
type Props = {
|
||||
t: string => string
|
||||
t: string => string,
|
||||
repositoriesLink: string,
|
||||
usersLink: string,
|
||||
groupsLink: string,
|
||||
configLink: string,
|
||||
logoutLink: string
|
||||
};
|
||||
|
||||
class PrimaryNavigation extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<nav className="tabs is-boxed">
|
||||
<ul>
|
||||
const { t, repositoriesLink, usersLink, groupsLink, configLink, logoutLink } = this.props;
|
||||
|
||||
const links = [
|
||||
repositoriesLink ? (
|
||||
<PrimaryNavigationLink
|
||||
to="/repos"
|
||||
match="/(repo|repos)"
|
||||
label={t("primary-navigation.repositories")}
|
||||
/>
|
||||
key={"repositoriesLink"}
|
||||
/>): null,
|
||||
usersLink ? (
|
||||
<PrimaryNavigationLink
|
||||
to="/users"
|
||||
match="/(user|users)"
|
||||
label={t("primary-navigation.users")}
|
||||
/>
|
||||
key={"usersLink"}
|
||||
/>) : null,
|
||||
groupsLink ? (
|
||||
<PrimaryNavigationLink
|
||||
to="/groups"
|
||||
match="/(group|groups)"
|
||||
label={t("primary-navigation.groups")}
|
||||
/>
|
||||
key={"groupsLink"}
|
||||
/>) : null,
|
||||
configLink ? (
|
||||
<PrimaryNavigationLink
|
||||
to="/config"
|
||||
label={t("primary-navigation.config")}
|
||||
/>
|
||||
key={"configLink"}
|
||||
/>) : null,
|
||||
logoutLink ? (
|
||||
<PrimaryNavigationLink
|
||||
to="/logout"
|
||||
label={t("primary-navigation.logout")}
|
||||
/>
|
||||
key={"logoutLink"}
|
||||
/>) : null
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="tabs is-boxed">
|
||||
<ul>
|
||||
{links}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { getProtocolLinkByType, getTypePredicate } from "./repositories";
|
||||
import { getProtocolLinkByType } from "./repositories";
|
||||
|
||||
describe("getProtocolLinkByType tests", () => {
|
||||
|
||||
|
||||
@@ -4,3 +4,26 @@ export const contextPath = window.ctxPath || "";
|
||||
export function withContextPath(path: string) {
|
||||
return contextPath + path;
|
||||
}
|
||||
|
||||
export function withEndingSlash(url: string) {
|
||||
if (url.endsWith("/")) {
|
||||
return url;
|
||||
}
|
||||
return url + "/";
|
||||
}
|
||||
|
||||
export function concat(base: string, ...parts: string[]) {
|
||||
let url = base;
|
||||
for ( let p of parts) {
|
||||
url = withEndingSlash(url) + p;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function getPageFromMatch(match: any) {
|
||||
let page = parseInt(match.params.page, 10);
|
||||
if (isNaN(page) || !page) {
|
||||
page = 1;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
49
scm-ui-components/packages/ui-components/src/urls.test.js
Normal file
49
scm-ui-components/packages/ui-components/src/urls.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import { concat, getPageFromMatch, withEndingSlash } from "./urls";
|
||||
|
||||
describe("tests for withEndingSlash", () => {
|
||||
|
||||
it("should append missing slash", () => {
|
||||
expect(withEndingSlash("abc")).toBe("abc/");
|
||||
});
|
||||
|
||||
it("should not append a second slash", () => {
|
||||
expect(withEndingSlash("abc/")).toBe("abc/");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("concat tests", () => {
|
||||
|
||||
it("should concat the parts to a single url", () => {
|
||||
expect(concat("a")).toBe("a");
|
||||
expect(concat("a", "b")).toBe("a/b");
|
||||
expect(concat("a", "b", "c")).toBe("a/b/c");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("tests for getPageFromMatch", () => {
|
||||
function createMatch(page: string) {
|
||||
return {
|
||||
params: {
|
||||
page
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
it("should return 1 for NaN", () => {
|
||||
const match = createMatch("any");
|
||||
expect(getPageFromMatch(match)).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 for 0", () => {
|
||||
const match = createMatch("0");
|
||||
expect(getPageFromMatch(match)).toBe(1);
|
||||
});
|
||||
|
||||
it("should return the given number", () => {
|
||||
const match = createMatch("42");
|
||||
expect(getPageFromMatch(match)).toBe(42);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
8011
scm-ui-components/packages/ui-components/yarn.lock
Normal file
8011
scm-ui-components/packages/ui-components/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
"check": "flow check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
8
scm-ui-components/packages/ui-types/src/Branches.js
Normal file
8
scm-ui-components/packages/ui-types/src/Branches.js
Normal file
@@ -0,0 +1,8 @@
|
||||
//@flow
|
||||
import type {Links} from "./hal";
|
||||
|
||||
export type Branch = {
|
||||
name: string,
|
||||
revision: string,
|
||||
_links: Links
|
||||
}
|
||||
25
scm-ui-components/packages/ui-types/src/Changesets.js
Normal file
25
scm-ui-components/packages/ui-types/src/Changesets.js
Normal file
@@ -0,0 +1,25 @@
|
||||
//@flow
|
||||
import type {Links} from "./hal";
|
||||
import type {Tag} from "./Tags";
|
||||
import type {Branch} from "./Branches";
|
||||
|
||||
export type Changeset = {
|
||||
id: string,
|
||||
date: Date,
|
||||
author: {
|
||||
name: string,
|
||||
mail?: string
|
||||
},
|
||||
description: string,
|
||||
_links: Links,
|
||||
_embedded: {
|
||||
tags?: Tag[],
|
||||
branches?: Branch[],
|
||||
parents?: ParentChangeset[]
|
||||
};
|
||||
}
|
||||
|
||||
export type ParentChangeset = {
|
||||
id: string,
|
||||
_links: Links
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//@flow
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type IndexResources = {
|
||||
version: string,
|
||||
_links: Links
|
||||
};
|
||||
@@ -1,14 +1,11 @@
|
||||
//@flow
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type Permission = {
|
||||
name: string,
|
||||
type: string,
|
||||
groupPermission: boolean,
|
||||
_links?: Links
|
||||
export type Permission = PermissionCreateEntry & {
|
||||
_links: Links
|
||||
};
|
||||
|
||||
export type PermissionEntry = {
|
||||
export type PermissionCreateEntry = {
|
||||
name: string,
|
||||
type: string,
|
||||
groupPermission: boolean
|
||||
|
||||
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[]
|
||||
}
|
||||
};
|
||||
8
scm-ui-components/packages/ui-types/src/Tags.js
Normal file
8
scm-ui-components/packages/ui-types/src/Tags.js
Normal file
@@ -0,0 +1,8 @@
|
||||
//@flow
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type Tag = {
|
||||
name: string,
|
||||
revision: string,
|
||||
_links: Links
|
||||
}
|
||||
@@ -4,10 +4,14 @@ export type Link = {
|
||||
name?: string
|
||||
};
|
||||
|
||||
export type Links = { [string]: Link | Link[] };
|
||||
type LinkValue = Link | Link[];
|
||||
|
||||
// TODO use LinkValue
|
||||
export type Links = { [string]: any };
|
||||
|
||||
export type Collection = {
|
||||
_embedded: Object,
|
||||
// $FlowFixMe
|
||||
_links: Links
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,16 @@ export type { Group, Member } from "./Group";
|
||||
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
|
||||
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||
|
||||
export type { Branch } from "./Branches";
|
||||
|
||||
export type { Changeset } from "./Changesets";
|
||||
|
||||
export type { Tag } from "./Tags";
|
||||
|
||||
export type { Config } from "./Config";
|
||||
|
||||
export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions";
|
||||
export type { IndexResources } from "./IndexResources";
|
||||
|
||||
export type { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions";
|
||||
|
||||
export type { SubRepository, File } from "./Sources";
|
||||
|
||||
@@ -707,9 +707,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -726,7 +726,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
"i18next-browser-languagedetector": "^2.2.2",
|
||||
"i18next-fetch-backend": "^0.1.0",
|
||||
"moment": "^2.22.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-i18next": "^7.9.0",
|
||||
"react-jss": "^8.6.0",
|
||||
"react-redux": "^5.0.7",
|
||||
@@ -44,13 +43,14 @@
|
||||
"pre-commit": "jest && flow && eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15",
|
||||
"@scm-manager/ui-bundler": "^0.0.17",
|
||||
"copyfiles": "^2.0.0",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"fetch-mock": "^6.5.0",
|
||||
"flow-typed": "^2.5.1",
|
||||
"jest": "^23.5.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"node-sass-chokidar": "^1.3.0",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"prettier": "^1.13.7",
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
"actions-label": "Actions",
|
||||
"back-label": "Back",
|
||||
"navigation-label": "Navigation",
|
||||
"history": "Commits",
|
||||
"information": "Information",
|
||||
"permissions": "Permissions"
|
||||
"permissions": "Permissions",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Repository",
|
||||
@@ -44,6 +46,32 @@
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"file-tree": {
|
||||
"name": "Name",
|
||||
"length": "Length",
|
||||
"lastModified": "Last modified",
|
||||
"description": "Description"
|
||||
}
|
||||
},
|
||||
"changesets": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Could not fetch changesets",
|
||||
"changeset": {
|
||||
"id": "ID",
|
||||
"description": "Description",
|
||||
"contact": "Contact",
|
||||
"date": "Date",
|
||||
"summary": "Changeset {{id}} was committed {{time}}"
|
||||
},
|
||||
"author": {
|
||||
"name": "Author",
|
||||
"mail": "Mail"
|
||||
}
|
||||
},
|
||||
"branch-selector": {
|
||||
"label": "Branches"
|
||||
},
|
||||
"permission": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown permissions error",
|
||||
@@ -67,6 +95,11 @@
|
||||
"add-permission-heading": "Add new Permission",
|
||||
"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!"
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -14,18 +14,20 @@ import {
|
||||
modifyConfigReset
|
||||
} from "../modules/config";
|
||||
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 { getConfigLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
config: Config,
|
||||
configUpdatePermission: boolean,
|
||||
configLink: string,
|
||||
|
||||
// dispatch functions
|
||||
modifyConfig: (config: Config, callback?: () => void) => void,
|
||||
fetchConfig: void => void,
|
||||
fetchConfig: (link: string) => void,
|
||||
configReset: void => void,
|
||||
|
||||
// context objects
|
||||
@@ -35,7 +37,7 @@ type Props = {
|
||||
class GlobalConfig extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.configReset();
|
||||
this.props.fetchConfig();
|
||||
this.props.fetchConfig(this.props.configLink);
|
||||
}
|
||||
|
||||
modifyConfig = (config: Config) => {
|
||||
@@ -75,8 +77,8 @@ class GlobalConfig extends React.Component<Props> {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchConfig: () => {
|
||||
dispatch(fetchConfig());
|
||||
fetchConfig: (link: string) => {
|
||||
dispatch(fetchConfig(link));
|
||||
},
|
||||
modifyConfig: (config: Config, callback?: () => void) => {
|
||||
dispatch(modifyConfig(config, callback));
|
||||
@@ -92,12 +94,14 @@ const mapStateToProps = state => {
|
||||
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
||||
const config = getConfig(state);
|
||||
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||
const configLink = getConfigLink(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
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_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
|
||||
|
||||
const CONFIG_URL = "config";
|
||||
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
|
||||
|
||||
//fetch config
|
||||
export function fetchConfig() {
|
||||
export function fetchConfig(link: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchConfigPending());
|
||||
return apiClient
|
||||
.get(CONFIG_URL)
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
|
||||
@@ -22,8 +22,10 @@ import reducer, {
|
||||
getConfig,
|
||||
getConfigUpdatePermission
|
||||
} 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!");
|
||||
|
||||
@@ -103,7 +105,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully fetch config", () => {
|
||||
fetchMock.getOnce(CONFIG_URL, response);
|
||||
fetchMock.getOnce(URL, response);
|
||||
|
||||
const expectedActions = [
|
||||
{ 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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail getting config on HTTP 500", () => {
|
||||
fetchMock.getOnce(CONFIG_URL, {
|
||||
fetchMock.getOnce(URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchConfig()).then(() => {
|
||||
const store = mockStore({
|
||||
indexResources: {
|
||||
links: {
|
||||
config: {
|
||||
href: CONFIG_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);
|
||||
|
||||
@@ -19,16 +19,33 @@ import {
|
||||
Footer,
|
||||
Header
|
||||
} 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 = {
|
||||
me: Me,
|
||||
authenticated: boolean,
|
||||
error: Error,
|
||||
loading: boolean,
|
||||
repositoriesLink: string,
|
||||
usersLink: string,
|
||||
groupsLink: string,
|
||||
configLink: string,
|
||||
logoutLink: string,
|
||||
meLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
fetchMe: () => void,
|
||||
fetchMe: (link: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -36,14 +53,37 @@ type Props = {
|
||||
|
||||
class App extends Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchMe();
|
||||
if (this.props.meLink) {
|
||||
this.props.fetchMe(this.props.meLink);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { me, loading, error, authenticated, t } = this.props;
|
||||
const {
|
||||
me,
|
||||
loading,
|
||||
error,
|
||||
authenticated,
|
||||
t,
|
||||
repositoriesLink,
|
||||
usersLink,
|
||||
groupsLink,
|
||||
configLink,
|
||||
logoutLink
|
||||
} = this.props;
|
||||
|
||||
let content;
|
||||
const navigation = authenticated ? <PrimaryNavigation /> : "";
|
||||
const navigation = authenticated ? (
|
||||
<PrimaryNavigation
|
||||
repositoriesLink={repositoriesLink}
|
||||
usersLink={usersLink}
|
||||
groupsLink={groupsLink}
|
||||
configLink={configLink}
|
||||
logoutLink={logoutLink}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
content = <Loading />;
|
||||
@@ -70,20 +110,34 @@ class App extends Component<Props> {
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchMe: () => dispatch(fetchMe())
|
||||
fetchMe: (link: string) => dispatch(fetchMe(link))
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const me = getMe(state);
|
||||
const loading = isFetchMePending(state);
|
||||
const error = getFetchMeFailure(state);
|
||||
const loading =
|
||||
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 {
|
||||
authenticated,
|
||||
me,
|
||||
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
|
||||
} from "@scm-manager/ui-components";
|
||||
import classNames from "classnames";
|
||||
import { getLoginLink } from "../modules/indexResource";
|
||||
|
||||
const styles = {
|
||||
avatar: {
|
||||
@@ -41,9 +42,10 @@ type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
link: string,
|
||||
|
||||
// dispatcher props
|
||||
login: (username: string, password: string) => void,
|
||||
login: (link: string, username: string, password: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string,
|
||||
@@ -74,7 +76,11 @@ class Login extends React.Component<Props, State> {
|
||||
handleSubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
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 loading = isLoginPending(state);
|
||||
const error = getLoginFailure(state);
|
||||
const link = getLoginLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
link
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
login: (username: string, password: string) =>
|
||||
dispatch(login(username, password))
|
||||
login: (loginLink: string, username: string, password: string) =>
|
||||
dispatch(login(loginLink, username, password))
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
getLogoutFailure
|
||||
} from "../modules/auth";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import { fetchIndexResources, getLogoutLink } from "../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
logoutLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
logout: () => void,
|
||||
logout: (link: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -26,7 +28,7 @@ type Props = {
|
||||
|
||||
class Logout extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.logout();
|
||||
this.props.logout(this.props.logoutLink);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -51,16 +53,18 @@ const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const loading = isLogoutPending(state);
|
||||
const error = getLogoutFailure(state);
|
||||
const logoutLink = getLogoutLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
logoutLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
logout: () => dispatch(logout())
|
||||
logout: (link: string) => dispatch(logout(link))
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
import { Route, Redirect, withRouter } from "react-router";
|
||||
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
|
||||
|
||||
import Overview from "../repos/containers/Overview";
|
||||
import Users from "../users/containers/Users";
|
||||
import Login from "../containers/Login";
|
||||
import Logout from "../containers/Logout";
|
||||
|
||||
import { Switch } from "react-router-dom";
|
||||
import { ProtectedRoute } from "@scm-manager/ui-components";
|
||||
import AddUser from "../users/containers/AddUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { apiClient, Loading } from "@scm-manager/ui-components";
|
||||
import { getUiPluginsLink } from "../modules/indexResource";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
children: React.Node,
|
||||
link: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -29,8 +32,13 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
this.setState({
|
||||
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(JSON.parse)
|
||||
.then(pluginCollection => pluginCollection._embedded.plugins)
|
||||
@@ -40,7 +48,7 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
finished: true
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadPlugins = (plugins: Plugin[]) => {
|
||||
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);
|
||||
|
||||
@@ -7,14 +7,18 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||
import users from "./users/modules/users";
|
||||
import repos from "./repos/modules/repos";
|
||||
import repositoryTypes from "./repos/modules/repositoryTypes";
|
||||
import changesets from "./repos/modules/changesets";
|
||||
import sources from "./repos/sources/modules/sources";
|
||||
import groups from "./groups/modules/groups";
|
||||
import auth from "./modules/auth";
|
||||
import pending from "./modules/pending";
|
||||
import failure from "./modules/failure";
|
||||
import permissions from "./repos/permissions/modules/permissions";
|
||||
import config from "./config/modules/config";
|
||||
import indexResources from "./modules/indexResource";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
import branches from "./repos/modules/branches";
|
||||
|
||||
function createReduxStore(history: BrowserHistory) {
|
||||
const composeEnhancers =
|
||||
@@ -24,13 +28,17 @@ function createReduxStore(history: BrowserHistory) {
|
||||
router: routerReducer,
|
||||
pending,
|
||||
failure,
|
||||
indexResources,
|
||||
users,
|
||||
repos,
|
||||
repositoryTypes,
|
||||
changesets,
|
||||
branches,
|
||||
permissions,
|
||||
groups,
|
||||
auth,
|
||||
config
|
||||
config,
|
||||
sources
|
||||
});
|
||||
|
||||
return createStore(
|
||||
|
||||
@@ -9,18 +9,21 @@ import {
|
||||
createGroup,
|
||||
isCreateGroupPending,
|
||||
getCreateGroupFailure,
|
||||
createGroupReset
|
||||
createGroupReset,
|
||||
getCreateGroupLink
|
||||
} from "../modules/groups";
|
||||
import type { Group } from "@scm-manager/ui-types";
|
||||
import type { History } from "history";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
createGroup: (group: Group, callback?: () => void) => void,
|
||||
createGroup: (link: string, group: Group, callback?: () => void) => void,
|
||||
history: History,
|
||||
loading?: boolean,
|
||||
error?: Error,
|
||||
resetForm: () => void
|
||||
resetForm: () => void,
|
||||
createLink: string
|
||||
};
|
||||
|
||||
type State = {};
|
||||
@@ -51,14 +54,14 @@ class AddGroup extends React.Component<Props, State> {
|
||||
this.props.history.push("/groups");
|
||||
};
|
||||
createGroup = (group: Group) => {
|
||||
this.props.createGroup(group, this.groupCreated);
|
||||
this.props.createGroup(this.props.createLink, group, this.groupCreated);
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
createGroup: (group: Group, callback?: () => void) =>
|
||||
dispatch(createGroup(group, callback)),
|
||||
createGroup: (link: string, group: Group, callback?: () => void) =>
|
||||
dispatch(createGroup(link, group, callback)),
|
||||
resetForm: () => {
|
||||
dispatch(createGroupReset());
|
||||
}
|
||||
@@ -68,7 +71,9 @@ const mapDispatchToProps = dispatch => {
|
||||
const mapStateToProps = state => {
|
||||
const loading = isCreateGroupPending(state);
|
||||
const error = getCreateGroupFailure(state);
|
||||
const createLink = getGroupsLink(state);
|
||||
return {
|
||||
createLink,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
isPermittedToCreateGroups,
|
||||
selectListAsCollection
|
||||
} from "../modules/groups";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
groups: Group[],
|
||||
@@ -26,19 +27,20 @@ type Props = {
|
||||
canAddGroups: boolean,
|
||||
list: PagedCollection,
|
||||
page: number,
|
||||
groupLink: string,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
// dispatch functions
|
||||
fetchGroupsByPage: (page: number) => void,
|
||||
fetchGroupsByPage: (link: string, page: number) => void,
|
||||
fetchGroupsByLink: (link: string) => void
|
||||
};
|
||||
|
||||
class Groups extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchGroupsByPage(this.props.page);
|
||||
this.props.fetchGroupsByPage(this.props.groupLink, this.props.page);
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {
|
||||
@@ -111,20 +113,23 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const canAddGroups = isPermittedToCreateGroups(state);
|
||||
const list = selectListAsCollection(state);
|
||||
|
||||
const groupLink = getGroupsLink(state);
|
||||
|
||||
return {
|
||||
groups,
|
||||
loading,
|
||||
error,
|
||||
canAddGroups,
|
||||
list,
|
||||
page
|
||||
page,
|
||||
groupLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchGroupsByPage: (page: number) => {
|
||||
dispatch(fetchGroupsByPage(page));
|
||||
fetchGroupsByPage: (link: string, page: number) => {
|
||||
dispatch(fetchGroupsByPage(link, page));
|
||||
},
|
||||
fetchGroupsByLink: (link: string) => {
|
||||
dispatch(fetchGroupsByLink(link));
|
||||
|
||||
@@ -26,16 +26,18 @@ import {
|
||||
|
||||
import { translate } from "react-i18next";
|
||||
import EditGroup from "./EditGroup";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
group: Group,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
groupLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
deleteGroup: (group: Group, callback?: () => void) => void,
|
||||
fetchGroup: string => void,
|
||||
fetchGroup: (string, string) => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
@@ -45,7 +47,7 @@ type Props = {
|
||||
|
||||
class SingleGroup extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchGroup(this.props.name);
|
||||
this.props.fetchGroup(this.props.groupLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
@@ -132,19 +134,21 @@ const mapStateToProps = (state, ownProps) => {
|
||||
isFetchGroupPending(state, name) || isDeleteGroupPending(state, name);
|
||||
const error =
|
||||
getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name);
|
||||
const groupLink = getGroupsLink(state);
|
||||
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
groupLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchGroup: (name: string) => {
|
||||
dispatch(fetchGroup(name));
|
||||
fetchGroup: (link: string, name: string) => {
|
||||
dispatch(fetchGroup(link, name));
|
||||
},
|
||||
deleteGroup: (group: Group, callback?: () => void) => {
|
||||
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_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
const GROUPS_URL = "groups";
|
||||
const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2";
|
||||
|
||||
// fetch groups
|
||||
export function fetchGroups() {
|
||||
return fetchGroupsByLink(GROUPS_URL);
|
||||
export function fetchGroups(link: string) {
|
||||
return fetchGroupsByLink(link);
|
||||
}
|
||||
|
||||
export function fetchGroupsByPage(page: number) {
|
||||
export function fetchGroupsByPage(link: string, page: number) {
|
||||
// backend start counting by 0
|
||||
return fetchGroupsByLink(GROUPS_URL + "?page=" + (page - 1));
|
||||
return fetchGroupsByLink(link + "?page=" + (page - 1));
|
||||
}
|
||||
|
||||
export function fetchGroupsByLink(link: string) {
|
||||
@@ -56,7 +55,7 @@ export function fetchGroupsByLink(link: string) {
|
||||
})
|
||||
.catch(cause => {
|
||||
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
|
||||
export function fetchGroup(name: string) {
|
||||
const groupUrl = GROUPS_URL + "/" + name;
|
||||
export function fetchGroup(link: string, name: string) {
|
||||
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchGroupPending(name));
|
||||
return apiClient
|
||||
@@ -132,11 +131,11 @@ export function fetchGroupFailure(name: string, error: Error): Action {
|
||||
}
|
||||
|
||||
//create group
|
||||
export function createGroup(group: Group, callback?: () => void) {
|
||||
export function createGroup(link: string, group: Group, callback?: () => void) {
|
||||
return function(dispatch: Dispatch) {
|
||||
dispatch(createGroupPending());
|
||||
return apiClient
|
||||
.post(GROUPS_URL, group, CONTENT_TYPE_GROUP)
|
||||
.post(link, group, CONTENT_TYPE_GROUP)
|
||||
.then(() => {
|
||||
dispatch(createGroupSuccess());
|
||||
if (callback) {
|
||||
@@ -410,6 +409,12 @@ export const isPermittedToCreateGroups = (state: Object): boolean => {
|
||||
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) {
|
||||
const groupNames = selectList(state).entries;
|
||||
if (!groupNames) {
|
||||
|
||||
@@ -42,9 +42,11 @@ import reducer, {
|
||||
modifyGroup,
|
||||
MODIFY_GROUP_PENDING,
|
||||
MODIFY_GROUP_SUCCESS,
|
||||
MODIFY_GROUP_FAILURE
|
||||
MODIFY_GROUP_FAILURE,
|
||||
getCreateGroupLink
|
||||
} from "./groups";
|
||||
const GROUPS_URL = "/api/v2/groups";
|
||||
const URL = "/groups";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -63,7 +65,7 @@ const humanGroup = {
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/v2/groups/humanGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -95,7 +97,7 @@ const emptyGroup = {
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -150,7 +152,7 @@ describe("groups fetch()", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchGroups()).then(() => {
|
||||
return store.dispatch(fetchGroups(URL)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -161,7 +163,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroups()).then(() => {
|
||||
return store.dispatch(fetchGroups(URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUPS_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUPS_FAILURE);
|
||||
@@ -173,7 +175,7 @@ describe("groups fetch()", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup("humanGroup")).then(() => {
|
||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||
@@ -187,7 +189,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup("humanGroup")).then(() => {
|
||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||
@@ -195,14 +197,13 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should successfully create group", () => {
|
||||
fetchMock.postOnce(GROUPS_URL, {
|
||||
status: 201
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
||||
return store.dispatch(createGroup(URL, humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||
@@ -219,7 +220,7 @@ describe("groups fetch()", () => {
|
||||
called = true;
|
||||
};
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(humanGroup, callMe)).then(() => {
|
||||
return store.dispatch(createGroup(URL, humanGroup, callMe)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||
@@ -227,14 +228,13 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail creating group on HTTP 500", () => {
|
||||
fetchMock.postOnce(GROUPS_URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
||||
return store.dispatch(createGroup(URL, humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE);
|
||||
@@ -337,13 +337,10 @@ describe("groups fetch()", () => {
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("groups reducer", () => {
|
||||
|
||||
it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => {
|
||||
|
||||
const newState = reducer({}, fetchGroupsSuccess(responseBody));
|
||||
|
||||
expect(newState.list).toEqual({
|
||||
@@ -391,7 +388,6 @@ describe("groups reducer", () => {
|
||||
expect(newState.byNames["humanGroup"]).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it("should update state according to FETCH_GROUP_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchGroupSuccess(emptyGroup));
|
||||
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
|
||||
@@ -426,7 +422,6 @@ describe("groups reducer", () => {
|
||||
expect(newState.byNames["emptyGroup"]).toBeFalsy();
|
||||
expect(newState.list.entries).toEqual(["humanGroup"]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
@@ -476,6 +471,23 @@ describe("selector tests", () => {
|
||||
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", () => {
|
||||
const state = {
|
||||
groups: {
|
||||
@@ -560,9 +572,13 @@ describe("selector tests", () => {
|
||||
});
|
||||
|
||||
it("should return true if create group is pending", () => {
|
||||
expect(isCreateGroupPending({pending: {
|
||||
expect(
|
||||
isCreateGroupPending({
|
||||
pending: {
|
||||
[CREATE_GROUP]: true
|
||||
}})).toBeTruthy();
|
||||
}
|
||||
})
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
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", () => {
|
||||
expect(getCreateGroupFailure({
|
||||
expect(
|
||||
getCreateGroupFailure({
|
||||
failure: {
|
||||
[CREATE_GROUP]: error
|
||||
}
|
||||
})).toEqual(error);
|
||||
})
|
||||
).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined if creating group did not fail", () => {
|
||||
expect(getCreateGroupFailure({})).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
it("should return true, when delete group humanGroup is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./containers/App";
|
||||
import Index from "./containers/Index";
|
||||
import registerServiceWorker from "./registerServiceWorker";
|
||||
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
@@ -37,9 +37,7 @@ ReactDOM.render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{/* ConnectedRouter will use the store from Provider automatically */}
|
||||
<ConnectedRouter history={history}>
|
||||
<PluginLoader>
|
||||
<App />
|
||||
</PluginLoader>
|
||||
<Index />
|
||||
</ConnectedRouter>
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
|
||||
@@ -5,6 +5,12 @@ import * as types from "./types";
|
||||
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
||||
import { isPending } from "./pending";
|
||||
import { getFailure } from "./failure";
|
||||
import {
|
||||
callFetchIndexResources,
|
||||
FETCH_INDEXRESOURCES_SUCCESS,
|
||||
fetchIndexResources, fetchIndexResourcesPending,
|
||||
fetchIndexResourcesSuccess
|
||||
} from "./indexResource";
|
||||
|
||||
// Action
|
||||
|
||||
@@ -121,16 +127,11 @@ export const fetchMeFailure = (error: Error) => {
|
||||
};
|
||||
};
|
||||
|
||||
// urls
|
||||
|
||||
const ME_URL = "/me";
|
||||
const LOGIN_URL = "/auth/access_token";
|
||||
|
||||
// side effects
|
||||
|
||||
const callFetchMe = (): Promise<Me> => {
|
||||
const callFetchMe = (link: string): Promise<Me> => {
|
||||
return apiClient
|
||||
.get(ME_URL)
|
||||
.get(link)
|
||||
.then(response => {
|
||||
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 = {
|
||||
cookie: true,
|
||||
grant_type: "password",
|
||||
@@ -149,9 +154,15 @@ export const login = (username: string, password: string) => {
|
||||
return function(dispatch: any) {
|
||||
dispatch(loginPending());
|
||||
return apiClient
|
||||
.post(LOGIN_URL, login_data)
|
||||
.post(loginLink, login_data)
|
||||
.then(response => {
|
||||
return callFetchMe();
|
||||
dispatch(fetchIndexResourcesPending())
|
||||
return callFetchIndexResources();
|
||||
})
|
||||
.then(response => {
|
||||
dispatch(fetchIndexResourcesSuccess(response));
|
||||
const meLink = response._links.me.href;
|
||||
return callFetchMe(meLink);
|
||||
})
|
||||
.then(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) {
|
||||
dispatch(fetchMePending());
|
||||
return callFetchMe()
|
||||
return callFetchMe(link)
|
||||
.then(me => {
|
||||
dispatch(fetchMeSuccess(me));
|
||||
})
|
||||
@@ -179,14 +190,17 @@ export const fetchMe = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
export const logout = (link: string) => {
|
||||
return function(dispatch: any) {
|
||||
dispatch(logoutPending());
|
||||
return apiClient
|
||||
.delete(LOGIN_URL)
|
||||
.delete(link)
|
||||
.then(() => {
|
||||
dispatch(logoutSuccess());
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchIndexResources());
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(logoutFailure(error));
|
||||
});
|
||||
|
||||
@@ -32,6 +32,10 @@ import reducer, {
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {
|
||||
FETCH_INDEXRESOURCES_PENDING,
|
||||
FETCH_INDEXRESOURCES_SUCCESS
|
||||
} from "./indexResource";
|
||||
|
||||
const me = { name: "tricia", displayName: "Tricia McMillian" };
|
||||
|
||||
@@ -93,14 +97,28 @@ describe("auth actions", () => {
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
|
||||
const meLink = {
|
||||
me: {
|
||||
href: "/me"
|
||||
}
|
||||
};
|
||||
|
||||
fetchMock.getOnce("/api/v2/", {
|
||||
_links: meLink
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: LOGIN_PENDING },
|
||||
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||
{ type: FETCH_INDEXRESOURCES_SUCCESS, payload: { _links: meLink } },
|
||||
{ type: LOGIN_SUCCESS, payload: me }
|
||||
];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -111,7 +129,9 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(login("tricia", "secret123")).then(() => {
|
||||
return store
|
||||
.dispatch(login("/auth/access_token", "tricia", "secret123"))
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(LOGIN_PENDING);
|
||||
expect(actions[1].type).toEqual(LOGIN_FAILURE);
|
||||
@@ -135,7 +155,7 @@ describe("auth actions", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchMe()).then(() => {
|
||||
return store.dispatch(fetchMe("me")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -146,7 +166,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchMe()).then(() => {
|
||||
return store.dispatch(fetchMe("me")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_ME_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
|
||||
@@ -166,7 +186,7 @@ describe("auth actions", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchMe()).then(() => {
|
||||
return store.dispatch(fetchMe("me")).then(() => {
|
||||
// return of async actions
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
@@ -181,14 +201,23 @@ describe("auth actions", () => {
|
||||
status: 401
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/v2/", {
|
||||
_links: {
|
||||
login: {
|
||||
login: "/login"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: LOGOUT_PENDING },
|
||||
{ type: LOGOUT_SUCCESS }
|
||||
{ type: LOGOUT_SUCCESS },
|
||||
{ type: FETCH_INDEXRESOURCES_PENDING }
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(logout()).then(() => {
|
||||
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -199,7 +228,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(logout()).then(() => {
|
||||
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(LOGOUT_PENDING);
|
||||
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);
|
||||
});
|
||||
});
|
||||
40
scm-ui/src/repos/components/DropDown.js
Normal file
40
scm-ui/src/repos/components/DropDown.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
options: string[],
|
||||
optionSelected: string => void,
|
||||
preselectedOption?: string,
|
||||
className: any
|
||||
};
|
||||
|
||||
class DropDown extends React.Component<Props> {
|
||||
render() {
|
||||
const { options, preselectedOption, className } = this.props;
|
||||
return (
|
||||
<div className={classNames(className, "select")}>
|
||||
<select
|
||||
value={preselectedOption ? preselectedOption : ""}
|
||||
onChange={this.change}
|
||||
>
|
||||
<option key="" />
|
||||
{options.map(option => {
|
||||
return (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||
this.props.optionSelected(event.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
@@ -4,7 +4,6 @@ import "../../tests/enzyme";
|
||||
import "../../tests/i18n";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import EditNavLink from "./EditNavLink";
|
||||
|
||||
describe("PermissionsNavLink", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
32
scm-ui/src/repos/components/changesets/AvatarImage.js
Normal file
32
scm-ui/src/repos/components/changesets/AvatarImage.js
Normal file
@@ -0,0 +1,32 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
import { Image } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
class AvatarImage extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
|
||||
const avatarFactory = binder.getExtension("changeset.avatar-factory");
|
||||
if (avatarFactory) {
|
||||
const avatar = avatarFactory(changeset);
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="has-rounded-border"
|
||||
src={avatar}
|
||||
alt={changeset.author.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarImage;
|
||||
18
scm-ui/src/repos/components/changesets/AvatarWrapper.js
Normal file
18
scm-ui/src/repos/components/changesets/AvatarWrapper.js
Normal file
@@ -0,0 +1,18 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
};
|
||||
|
||||
class AvatarWrapper extends React.Component<Props> {
|
||||
render() {
|
||||
if (binder.hasExtension("changeset.avatar-factory")) {
|
||||
return <>{this.props.children}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarWrapper;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user