merge 2.0.0-m3

This commit is contained in:
Maren Süwer
2018-10-25 08:14:46 +02:00
199 changed files with 15416 additions and 10282 deletions

View File

@@ -71,7 +71,7 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
} }
@Override @Override
public void delete(T object) throws NotFoundException { public void delete(T object){
decorated.delete(object); decorated.delete(object);
} }
@@ -82,12 +82,12 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
} }
@Override @Override
public void modify(T object) throws NotFoundException { public void modify(T object){
decorated.modify(object); decorated.modify(object);
} }
@Override @Override
public void refresh(T object) throws NotFoundException { public void refresh(T object){
decorated.refresh(object); decorated.refresh(object);
} }

View File

@@ -1,6 +1,6 @@
package sonia.scm; package sonia.scm;
public class NotFoundException extends Exception { public class NotFoundException extends RuntimeException {
public NotFoundException(String type, String id) { public NotFoundException(String type, String id) {
super(type + " with id '" + id + "' not found"); super(type + " with id '" + id + "' not found");
} }

View File

@@ -1,10 +1,10 @@
/** /**
* Copyright (c) 2010, Sebastian Sdorra * Copyright (c) 2010, Sebastian Sdorra
* All rights reserved. * All rights reserved.
* * <p>
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
* * <p>
* 1. Redistributions of source code must retain the above copyright notice, * 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, * 2. Redistributions in binary form must reproduce the above copyright notice,
@@ -13,7 +13,7 @@
* 3. Neither the name of SCM-Manager; nor the names of its * 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this * contributors may be used to endorse or promote products derived from this
* software without specific prior written permission. * software without specific prior written permission.
* * <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,13 +24,11 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* * <p>
* http://bitbucket.org/sdorra/scm-manager * http://bitbucket.org/sdorra/scm-manager
*
*/ */
package sonia.scm.repository; package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
@@ -40,12 +38,8 @@ import com.google.common.base.Objects;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable; import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -56,224 +50,56 @@ import java.util.List;
*/ */
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "browser-result") @XmlRootElement(name = "browser-result")
public class BrowserResult implements Iterable<FileObject>, Serializable public class BrowserResult implements Serializable {
{
/** Field description */ private String revision;
private static final long serialVersionUID = 2818662048045182761L; private FileObject file;
//~--- constructors --------------------------------------------------------- public BrowserResult() {
/**
* Constructs ...
*
*/
public BrowserResult() {}
/**
* Constructs ...
*
*
* @param revision
* @param tag
* @param branch
* @param files
*/
public BrowserResult(String revision, String tag, String branch,
List<FileObject> files)
{
this.revision = revision;
this.tag = tag;
this.branch = branch;
this.files = files;
} }
//~--- methods -------------------------------------------------------------- public BrowserResult(String revision, FileObject file) {
this.revision = revision;
this.file = file;
}
public String getRevision() {
return revision;
}
public FileObject getFile() {
return file;
}
/**
* {@inheritDoc}
*
*
* @param obj
*
* @return
*/
@Override @Override
public boolean equals(Object obj) public boolean equals(Object obj) {
{ if (obj == null) {
if (obj == null)
{
return false; return false;
} }
if (getClass() != obj.getClass()) if (getClass() != obj.getClass()) {
{
return false; return false;
} }
final BrowserResult other = (BrowserResult) obj; final BrowserResult other = (BrowserResult) obj;
return Objects.equal(revision, other.revision) return Objects.equal(revision, other.revision)
&& Objects.equal(tag, other.tag) && Objects.equal(file, other.file);
&& Objects.equal(branch, other.branch)
&& Objects.equal(files, other.files);
} }
/**
* {@inheritDoc}
*
*
* @return
*/
@Override @Override
public int hashCode() public int hashCode() {
{ return Objects.hashCode(revision, file);
return Objects.hashCode(revision, tag, branch, files);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public Iterator<FileObject> iterator() public String toString() {
{
Iterator<FileObject> it = null;
if (files != null)
{
it = files.iterator();
}
return it;
}
/**
* {@inheritDoc}
*
*
* @return
*/
@Override
public String toString()
{
//J-
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("revision", revision) .add("revision", revision)
.add("tag", tag) .add("files", file)
.add("branch", branch)
.add("files", files)
.toString(); .toString();
//J+
} }
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getBranch()
{
return branch;
}
/**
* Method description
*
*
* @return
*/
public List<FileObject> getFiles()
{
return files;
}
/**
* Method description
*
*
* @return
*/
public String getRevision()
{
return revision;
}
/**
* Method description
*
*
* @return
*/
public String getTag()
{
return tag;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param branch
*/
public void setBranch(String branch)
{
this.branch = branch;
}
/**
* Method description
*
*
* @param files
*/
public void setFiles(List<FileObject> files)
{
this.files = files;
}
/**
* Method description
*
*
* @param revision
*/
public void setRevision(String revision)
{
this.revision = revision;
}
/**
* Method description
*
*
* @param tag
*/
public void setTag(String tag)
{
this.tag = tag;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String branch;
/** Field description */
@XmlElement(name = "file")
@XmlElementWrapper(name = "files")
private List<FileObject> files;
/** Field description */
private String revision;
/** Field description */
private String tag;
} }

View File

@@ -33,10 +33,9 @@
package sonia.scm.repository; package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.base.Strings;
import sonia.scm.LastModifiedAware; import sonia.scm.LastModifiedAware;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
@@ -44,8 +43,11 @@ import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------ import static java.util.Collections.unmodifiableCollection;
/** /**
* The FileObject represents a file or a directory in a repository. * The FileObject represents a file or a directory in a repository.
@@ -181,6 +183,22 @@ public class FileObject implements LastModifiedAware, Serializable
return path; return path;
} }
/**
* Returns the parent path of the file.
*
* @return parent path
*/
public String getParentPath() {
if (Strings.isNullOrEmpty(path)) {
return null;
}
int index = path.lastIndexOf('/');
if (index > 0) {
return path.substring(0, index);
}
return "";
}
/** /**
* Return sub repository informations or null if the file is not * Return sub repository informations or null if the file is not
* sub repository. * sub repository.
@@ -284,6 +302,22 @@ public class FileObject implements LastModifiedAware, Serializable
this.subRepository = subRepository; this.subRepository = subRepository;
} }
public Collection<FileObject> getChildren() {
return unmodifiableCollection(children);
}
public void setChildren(List<FileObject> children) {
this.children = new ArrayList<>(children);
}
public void addChild(FileObject child) {
this.children.add(child);
}
public boolean hasChildren() {
return !children.isEmpty();
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** file description */ /** file description */
@@ -307,4 +341,6 @@ public class FileObject implements LastModifiedAware, Serializable
/** sub repository informations */ /** sub repository informations */
@XmlElement(name = "subrepository") @XmlElement(name = "subrepository")
private SubRepository subRepository; private SubRepository subRepository;
private Collection<FileObject> children = new ArrayList<>();
} }

View File

@@ -161,11 +161,21 @@ public class PreProcessorUtil
{ {
if (logger.isTraceEnabled()) if (logger.isTraceEnabled())
{ {
logger.trace("prepare browser result of repository {} for return", logger.trace("prepare browser result of repository {} for return", repository.getName());
repository.getName());
} }
handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet); PreProcessorHandler<FileObject> handler = new PreProcessorHandler<>(fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet, repository);
handlePreProcessorForFileObject(handler, result.getFile());
}
private void handlePreProcessorForFileObject(PreProcessorHandler<FileObject> handler, FileObject fileObject) {
if (fileObject.isDirectory()) {
for (FileObject child : fileObject.getChildren()) {
handlePreProcessorForFileObject(handler, child);
}
}
handler.callPreProcessorFactories(fileObject);
handler.callPreProcessors(fileObject);
} }
/** /**

View File

@@ -38,11 +38,11 @@ package sonia.scm.repository.api;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.FileObjectNameComparator;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey; import sonia.scm.repository.RepositoryCacheKey;
@@ -52,8 +52,6 @@ import sonia.scm.repository.spi.BrowseCommandRequest;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -138,7 +136,7 @@ public final class BrowseCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public BrowserResult getBrowserResult() throws IOException, RevisionNotFoundException { public BrowserResult getBrowserResult() throws IOException, NotFoundException {
BrowserResult result = null; BrowserResult result = null;
if (disableCache) if (disableCache)
@@ -180,14 +178,6 @@ public final class BrowseCommandBuilder
if (!disablePreProcessors && (result != null)) if (!disablePreProcessors && (result != null))
{ {
preProcessorUtil.prepareForReturn(repository, result); preProcessorUtil.prepareForReturn(repository, result);
List<FileObject> fileObjects = result.getFiles();
if (fileObjects != null)
{
Collections.sort(fileObjects, FileObjectNameComparator.instance);
result.setFiles(fileObjects);
}
} }
return result; return result;

View File

@@ -35,8 +35,8 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
@@ -60,4 +60,5 @@ public interface BrowseCommand
* *
* @throws IOException * @throws IOException
*/ */
BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, RevisionNotFoundException;} BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, NotFoundException;
}

View File

@@ -2,10 +2,10 @@ package sonia.scm.user;
public class ChangePasswordNotAllowedException extends RuntimeException { 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) { public ChangePasswordNotAllowedException(String type) {
super(message); super(String.format(WRONG_USER_TYPE, type));
} }
} }

View File

@@ -2,9 +2,7 @@ package sonia.scm.user;
public class InvalidPasswordException extends RuntimeException { public class InvalidPasswordException extends RuntimeException {
public static final String INVALID_MATCHING = "The given Password does not match with the stored one."; public InvalidPasswordException() {
super("The given Password does not match with the stored one.");
public InvalidPasswordException(String message) {
super(message);
} }
} }

View File

@@ -56,7 +56,10 @@ import java.security.Principal;
* *
* @author Sebastian Sdorra * @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") @XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
@@ -274,10 +277,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
//J+ //J+
} }
public User changePassword(String password){
setPassword(password);
return this;
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
/** /**

View File

@@ -38,11 +38,7 @@ package sonia.scm.user;
import sonia.scm.Manager; import sonia.scm.Manager;
import sonia.scm.search.Searchable; import sonia.scm.search.Searchable;
import java.text.MessageFormat;
import java.util.Collection; 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. * The central class for managing {@link User} objects.
@@ -75,18 +71,6 @@ public interface UserManager
*/ */
public String getDefaultType(); 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) { default boolean isTypeDefault(User user) {
return getDefaultType().equals(user.getType()); return getDefaultType().equals(user.getType());
} }
@@ -99,5 +83,17 @@ public interface UserManager
*/ */
Collection<User> autocomplete(String filter); 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);
} }

View File

@@ -126,6 +126,15 @@ public class UserManagerDecorator extends ManagerDecorator<User>
return decorated.autocomplete(filter); return decorated.autocomplete(filter);
} }
@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 --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */

View File

@@ -39,6 +39,8 @@ public class VndMediaType {
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX; public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
@SuppressWarnings("squid:S2068") @SuppressWarnings("squid:S2068")
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX; 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 ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX;

View File

@@ -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;
}
}

View File

@@ -154,7 +154,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(Group)} with an existing group. * Tests {@link SyncingRealmHelper#store(Group)} with an existing group.
*/ */
@Test @Test
public void testStoreGroupModify() throws NotFoundException { public void testStoreGroupModify(){
Group group = new Group("unit-test", "heartOfGold"); Group group = new Group("unit-test", "heartOfGold");
when(groupManager.get("heartOfGold")).thenReturn(group); when(groupManager.get("heartOfGold")).thenReturn(group);
@@ -191,7 +191,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(User)} with an existing user. * Tests {@link SyncingRealmHelper#store(User)} with an existing user.
*/ */
@Test @Test
public void testStoreUserModify() throws NotFoundException { public void testStoreUserModify(){
when(userManager.contains("tricia")).thenReturn(Boolean.TRUE); when(userManager.contains("tricia")).thenReturn(Boolean.TRUE);
User user = new User("tricia"); User user = new User("tricia");

View 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)));
}
}

View 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");
}
}

View File

@@ -20,12 +20,9 @@ public class MeITCase {
String newPassword = TestData.USER_SCM_ADMIN + "1"; String newPassword = TestData.USER_SCM_ADMIN + "1";
// admin change the own password // admin change the own password
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.url(TestData.getMeUrl()) .requestMe()
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getMeResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull) .assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo("xml")) .assertType(s -> assertThat(s).isEqualTo("xml"))
@@ -33,30 +30,48 @@ public class MeITCase {
.assertStatusCode(204); .assertStatusCode(204);
// assert password is changed -> login with the new Password than undo changes // assert password is changed -> login with the new Password than undo changes
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(TestData.USER_SCM_ADMIN, newPassword)
.url(TestData.getUserUrl(TestData.USER_SCM_ADMIN)) .requestMe()
.usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
.getMeResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN) .requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
.assertStatusCode(204); .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 @Test
public void shouldHidePasswordLinkIfUserTypeIsNotXML() { public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
String newUser = "user"; String newUser = "user";
String password = "pass"; String password = "pass";
String type = "not XML Type"; String type = "not XML Type";
TestData.createUser(newUser, password, true, type); TestData.createUser(newUser, password, true, type, "user@scm-manager.org");
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(newUser, password)
.url(TestData.getMeUrl()) .requestMe()
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull) .assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type)) .assertType(s -> assertThat(s).isEqualTo(type))

View File

@@ -87,13 +87,13 @@ public class PermissionsITCase {
@Before @Before
public void prepareEnvironment() { public void prepareEnvironment() {
TestData.createDefault(); TestData.createDefault();
TestData.createUser(USER_READ, USER_PASS); TestData.createNotAdminUser(USER_READ, USER_PASS);
TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType); 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.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.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
TestData.createUser(USER_OTHER, USER_PASS); TestData.createNotAdminUser(USER_OTHER, USER_PASS);
createdPermissions = 3; createdPermissions = 3;
} }

View File

@@ -44,7 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType; private final String repositoryType;
private File folder; private File folder;
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest; private ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> repositoryResponse;
public RepositoryAccessITCase(String repositoryType) { public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType; this.repositoryType = repositoryType;
@@ -59,17 +59,13 @@ public class RepositoryAccessITCase {
public void init() { public void init() {
TestData.createDefault(); TestData.createDefault();
folder = tempFolder.getRoot(); folder = tempFolder.getRoot();
repositoryGetRequest = ScmRequests.start() String namespace = ADMIN_USERNAME;
.given() String repo = TestData.getDefaultRepoName(repositoryType);
.url(TestData.getDefaultRepositoryUrl(repositoryType)) repositoryResponse =
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) ScmRequests.start()
.getRepositoryResource() .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK); .assertStatusCode(HttpStatus.SC_OK);
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.getMeResource();
} }
@Test @Test
@@ -201,7 +197,7 @@ public class RepositoryAccessITCase {
.then() .then()
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.extract() .extract()
.path("_embedded.files.find{it.name=='a.txt'}._links.self.href"); .path("_embedded.children.find{it.name=='a.txt'}._links.self.href");
given() given()
.when() .when()
@@ -216,7 +212,7 @@ public class RepositoryAccessITCase {
.then() .then()
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.extract() .extract()
.path("_embedded.files.find{it.name=='subfolder'}._links.self.href"); .path("_embedded.children.find{it.name=='subfolder'}._links.self.href");
String selfOfSubfolderUrl = given() String selfOfSubfolderUrl = given()
.when() .when()
.get(subfolderSourceUrl) .get(subfolderSourceUrl)
@@ -231,7 +227,7 @@ public class RepositoryAccessITCase {
.then() .then()
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.extract() .extract()
.path("_embedded.files[0]._links.self.href"); .path("_embedded.children[0]._links.self.href");
given() given()
.when() .when()
.get(subfolderContentUrl) .get(subfolderContentUrl)
@@ -306,17 +302,12 @@ public class RepositoryAccessITCase {
public void shouldFindFileHistory() throws IOException { public void shouldFindFileHistory() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a"); Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a");
repositoryGetRequest repositoryResponse
.usingRepositoryResponse()
.requestSources() .requestSources()
.usingSourcesResponse()
.requestSelf("folder") .requestSelf("folder")
.usingSourcesResponse()
.requestSelf("subfolder") .requestSelf("subfolder")
.usingSourcesResponse()
.requestFileHistory("a.txt") .requestFileHistory("a.txt")
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.assertChangesets(changesets -> { .assertChangesets(changesets -> {
assertThat(changesets).hasSize(1); assertThat(changesets).hasSize(1);
assertThat(changesets.get(0)).containsEntry("id", changeset.getId()); assertThat(changesets.get(0)).containsEntry("id", changeset.getId());
@@ -332,14 +323,11 @@ public class RepositoryAccessITCase {
String fileName = "a.txt"; String fileName = "a.txt";
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a"); Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
String revision = changeset.getId(); String revision = changeset.getId();
repositoryGetRequest repositoryResponse
.usingRepositoryResponse()
.requestChangesets() .requestChangesets()
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision) .requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(addedFiles -> assertThat(addedFiles) .assertAdded(addedFiles -> assertThat(addedFiles)
.hasSize(1) .hasSize(1)
@@ -359,14 +347,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName); Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName);
String revision = changeset.getId(); String revision = changeset.getId();
repositoryGetRequest repositoryResponse
.usingRepositoryResponse()
.requestChangesets() .requestChangesets()
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision) .requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertRemoved(removedFiles -> assertThat(removedFiles) .assertRemoved(removedFiles -> assertThat(removedFiles)
.hasSize(1) .hasSize(1)
@@ -386,14 +371,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content"); Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content");
String revision = changeset.getId(); String revision = changeset.getId();
repositoryGetRequest repositoryResponse
.usingRepositoryResponse()
.requestChangesets() .requestChangesets()
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision) .requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertModified(modifiedFiles -> assertThat(modifiedFiles) .assertModified(modifiedFiles -> assertThat(modifiedFiles)
.hasSize(1) .hasSize(1)
@@ -423,14 +405,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
String revision = changeset.getId(); String revision = changeset.getId();
repositoryGetRequest repositoryResponse
.usingRepositoryResponse()
.requestChangesets() .requestChangesets()
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision) .requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(a -> assertThat(a) .assertAdded(a -> assertThat(a)
.hasSize(1) .hasSize(1)
@@ -463,14 +442,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
String revision = changeset.getId(); String revision = changeset.getId();
repositoryGetRequest repositoryResponse
.usingRepositoryResponse()
.requestChangesets() .requestChangesets()
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision) .requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK) .assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(a -> assertThat(a) .assertAdded(a -> assertThat(a)
.hasSize(3) .hasSize(3)

View File

@@ -19,75 +19,83 @@ public class UserITCase {
public void adminShouldChangeOwnPassword() { public void adminShouldChangeOwnPassword() {
String newUser = "user"; String newUser = "user";
String password = "pass"; String password = "pass";
TestData.createUser(newUser, password, true, "xml"); TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org");
String newPassword = "new_password"; String newPassword = "new_password";
// admin change the own password // admin change the own password
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(newUser, password)
.url(TestData.getUserUrl(newUser)) .assertStatusCode(200)
.usernameAndPassword(newUser, password) .requestUser(newUser)
.getUserResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull) .assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource .requestChangePassword(newPassword)
.assertStatusCode(204); .assertStatusCode(204);
// assert password is changed -> login with the new Password // assert password is changed -> login with the new Password
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(newUser, newPassword)
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, newPassword)
.getUserResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingUserResponse() .requestUser(newUser)
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE)) .assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull); .assertPassword(Assert::assertNull);
} }
@Test @Test
public void adminShouldChangePasswordOfOtherUser() { public void adminShouldChangePasswordOfOtherUser() {
String newUser = "user"; String newUser = "user";
String password = "pass"; String password = "pass";
TestData.createUser(newUser, password, true, "xml"); TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org");
String newPassword = "new_password"; String newPassword = "new_password";
// admin change the password of the user // admin change the password of the user
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.url(TestData.getUserUrl(newUser))// the admin get the user object .assertStatusCode(200)
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) .requestUser(newUser)
.getUserResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
.assertPassword(Assert::assertNull) .assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204); .assertStatusCode(204);
// assert password is changed // assert password is changed
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(newUser, newPassword)
.url(TestData.getUserUrl(newUser)) .assertStatusCode(200)
.usernameAndPassword(newUser, newPassword) .requestUser(newUser)
.getUserResource()
.assertStatusCode(200); .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 @Test
public void shouldHidePasswordLinkIfUserTypeIsNotXML() { public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
String newUser = "user"; String newUser = "user";
String password = "pass"; String password = "pass";
String type = "not XML Type"; String type = "not XML Type";
TestData.createUser(newUser, password, true, type); TestData.createUser(newUser, password, true, type, "user@scm-manager.org");
ScmRequests.start() ScmRequests.start()
.given() .requestIndexResource(newUser, password)
.url(TestData.getMeUrl()) .assertStatusCode(200)
.usernameAndPassword(newUser, password) .requestUser(newUser)
.getUserResource()
.assertStatusCode(200) .assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull) .assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type)) .assertType(s -> assertThat(s).isEqualTo(type))

View File

@@ -2,9 +2,13 @@ package sonia.scm.it.utils;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.response.Response; import io.restassured.response.Response;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.user.User;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import java.net.URI; import java.net.ConnectException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -25,7 +29,8 @@ import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
*/ */
public class ScmRequests { public class ScmRequests {
private String url; private static final Logger LOG = LoggerFactory.getLogger(ScmRequests.class);
private String username; private String username;
private String password; private String password;
@@ -33,10 +38,29 @@ public class ScmRequests {
return new ScmRequests(); return new ScmRequests();
} }
public Given given() { public IndexResponse requestIndexResource(String username, String password) {
return new Given(); 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 * 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 * @return the response of the GET request using the given link
*/ */
private Response applyGETRequestFromLink(Response response, String linkPropertyName) { private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
return applyGETRequest(response return applyGETRequestFromLinkWithParams(response, linkPropertyName, "");
.then()
.extract()
.path(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. * Apply a GET Request to the given <code>url</code> and return the response.
* *
* @param url the url of the GET request * @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code> * @return the response of the GET request using the given <code>url</code>
*/ **/
private Response applyGETRequest(String url) { private Response applyGETRequest(String url) {
return RestAssured.given() return applyGETRequestWithQueryParams(url, "");
.auth().preemptive().basic(username, password)
.when()
.get(url);
} }
@@ -92,6 +146,7 @@ public class ScmRequests {
* @return the response of the PUT request using the given <code>url</code> * @return the response of the PUT request using the given <code>url</code>
*/ */
private Response applyPUTRequest(String url, String mediaType, String body) { private Response applyPUTRequest(String url, String mediaType, String body) {
LOG.info("PUT {}", url);
return RestAssured.given() return RestAssured.given()
.auth().preemptive().basic(username, password) .auth().preemptive().basic(username, password)
.when() .when()
@@ -101,11 +156,6 @@ public class ScmRequests {
.put(url); .put(url);
} }
private void setUrl(String url) {
this.url = url;
}
private void setUsername(String username) { private void setUsername(String username) {
this.username = username; this.username = username;
} }
@@ -114,272 +164,163 @@ public class ScmRequests {
this.password = password; this.password = password;
} }
private String getUrl() { public class IndexResponse extends ModelResponse<IndexResponse, IndexResponse> {
return url; 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() { public AutoCompleteResponse<IndexResponse> requestAutoCompleteUsers(String q) {
return username; return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_USERS, "?q=" + q), this);
} }
private String getPassword() { public AutoCompleteResponse<IndexResponse> requestAutoCompleteGroups(String q) {
return password; return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_GROUPS, "?q=" + q), this);
} }
public class Given { public RepositoryResponse<IndexResponse> requestRepository(String namespace, String name) {
return new RepositoryResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORIES, namespace + "/" + name), this);
public GivenUrl url(String url) {
setUrl(url);
return new GivenUrl();
} }
public GivenUrl url(URI url) { public MeResponse<IndexResponse> requestMe() {
setUrl(url.toString()); return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this);
return new GivenUrl(); }
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 class ChangesetsResponse<PREV extends ModelResponse> extends ModelResponse<ChangesetsResponse<PREV>, PREV> {
public AppliedMeRequest getMeResource() {
return new AppliedMeRequest(applyGETRequest(url));
}
public AppliedUserRequest getUserResource() { public ChangesetsResponse(Response response, PREV previousResponse) {
return new AppliedUserRequest(applyGETRequest(url)); super(response, previousResponse);
}
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 assertChangesets(Consumer<List<Map>> changesetsConsumer) { 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); changesetsConsumer.accept(changesets);
return this; return this;
} }
public AppliedDiffRequest requestDiff(String revision) { public DiffResponse<ChangesetsResponse> requestDiff(String revision) {
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href")); return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this);
} }
public AppliedModificationsRequest requestModifications(String revision) { public ModificationsResponse<ChangesetsResponse> requestModifications(String revision) {
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href")); 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) { public class SourcesResponse<PREV extends ModelResponse> extends ModelResponse<SourcesResponse<PREV>, PREV> {
super(sourcesResponse);
}
public SourcesResponse usingSourcesResponse() { public SourcesResponse(Response response, PREV previousResponse) {
return new SourcesResponse(super.response); super(response, previousResponse);
}
}
public class SourcesResponse {
private Response sourcesResponse;
public SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
} }
public SourcesResponse assertRevision(Consumer<String> assertRevision) { public SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision"); String revision = response.then().extract().path("revision");
assertRevision.accept(revision); assertRevision.accept(revision);
return this; return this;
} }
public SourcesResponse assertFiles(Consumer<List> assertFiles) { public SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files"); List files = response.then().extract().path("files");
assertFiles.accept(files); assertFiles.accept(files);
return this; return this;
} }
public AppliedChangesetsRequest requestFileHistory(String fileName) { public ChangesetsResponse<SourcesResponse> requestFileHistory(String fileName) {
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href")); return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.children.find{it.name=='" + fileName + "'}._links.history.href"), this);
} }
public AppliedSourcesRequest requestSelf(String fileName) { public SourcesResponse<SourcesResponse> requestSelf(String fileName) {
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href")); 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) { public ModificationsResponse(Response response, PREV previousResponse) {
super(response); super(response, previousResponse);
}
} }
public class GivenUrl { public ModificationsResponse<PREV> assertRevision(Consumer<String> assertRevision) {
String revision = response.then().extract().path("revision");
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");
assertRevision.accept(revision); assertRevision.accept(revision);
return this; return this;
} }
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) { public ModificationsResponse<PREV> assertAdded(Consumer<List<String>> assertAdded) {
List<String> added = resource.then().extract().path("added"); List<String> added = response.then().extract().path("added");
assertAdded.accept(added); assertAdded.accept(added);
return this; return this;
} }
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) { public ModificationsResponse<PREV> assertRemoved(Consumer<List<String>> assertRemoved) {
List<String> removed = resource.then().extract().path("removed"); List<String> removed = response.then().extract().path("removed");
assertRemoved.accept(removed); assertRemoved.accept(removed);
return this; return this;
} }
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) { public ModificationsResponse<PREV> assertModified(Consumer<List<String>> assertModified) {
List<String> modified = resource.then().extract().path("modified"); List<String> modified = response.then().extract().path("modified");
assertModified.accept(modified); assertModified.accept(modified);
return this; 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() { public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
return new MeResponse(super.response); return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
}
} }
} public class UserResponse<SELF extends UserResponse<SELF, PREV>, PREV extends ModelResponse> extends ModelResponse<SELF, PREV> {
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 static final String LINKS_PASSWORD_HREF = "_links.password.href"; public static final String LINKS_PASSWORD_HREF = "_links.password.href";
public UserResponse(Response response) { public UserResponse(Response response, PREV previousResponse) {
super(response); super(response, previousResponse);
} }
public SELF assertPassword(Consumer<String> assertPassword) { public SELF assertPassword(Consumer<String> assertPassword) {
@@ -402,22 +343,27 @@ public class ScmRequests {
return assertPropertyPathExists(LINKS_PASSWORD_HREF); return assertPropertyPathExists(LINKS_PASSWORD_HREF);
} }
public AppliedChangePasswordRequest requestChangePassword(String newPassword) { public ChangePasswordResponse<UserResponse> requestChangePassword(String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword))); return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_OVERWRITE, createPasswordChangeJson(null, newPassword)), this);
} }
} }
/** /**
* encapsulate standard assertions over model properties * 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; protected Response response;
public ModelResponse(Response response) { public ModelResponse(Response response, PREV previousResponse) {
this.response = response; this.response = response;
this.previousResponse = previousResponse;
}
public PREV returnToPrevious() {
return previousResponse;
} }
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) { public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
@@ -441,25 +387,45 @@ public class ScmRequests {
assertProperties.accept(properties); assertProperties.accept(properties);
return (SELF) this; 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) { public AutoCompleteResponse(Response response, PREV previousResponse) {
super(response); 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) { public class DiffResponse<PREV extends ModelResponse> extends ModelResponse<DiffResponse<PREV>, PREV> {
super(response);
public DiffResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
} }
public UserResponse usingUserResponse() { public class ChangePasswordResponse<PREV extends ModelResponse> extends ModelResponse<ChangePasswordResponse<PREV>, PREV> {
return new UserResponse(super.response);
}
public ChangePasswordResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
} }
} }

View File

@@ -46,11 +46,11 @@ public class TestData {
return DEFAULT_REPOSITORIES.get(repositoryType); return DEFAULT_REPOSITORIES.get(repositoryType);
} }
public static void createUser(String username, String password) { public static void createNotAdminUser(String username, String password) {
createUser(username, password, false, "xml"); 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); LOG.info("create user with username: {}", username);
String admin = isAdmin ? "true" : "false"; String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER) given(VndMediaType.USER)
@@ -61,7 +61,7 @@ public class TestData {
.append(" \"admin\": ").append(admin).append(",\n") .append(" \"admin\": ").append(admin).append(",\n")
.append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n") .append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
.append(" \"displayName\": \"").append(username).append("\",\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(" \"name\": \"").append(username).append("\",\n")
.append(" \"password\": \"").append(password).append("\",\n") .append(" \"password\": \"").append(password).append("\",\n")
.append(" \"type\": \"").append(type).append("\"\n") .append(" \"type\": \"").append(type).append("\"\n")
@@ -71,6 +71,16 @@ public class TestData {
.statusCode(HttpStatus.SC_CREATED) .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) { public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
@@ -193,28 +203,31 @@ public class TestData {
return JSON_BUILDER return JSON_BUILDER
.add("contact", "zaphod.beeblebrox@hitchhiker.com") .add("contact", "zaphod.beeblebrox@hitchhiker.com")
.add("description", "Heart of Gold") .add("description", "Heart of Gold")
.add("name", "HeartOfGold-" + repositoryType) .add("name", getDefaultRepoName(repositoryType))
.add("archived", false) .add("archived", false)
.add("type", repositoryType) .add("type", repositoryType)
.build().toString(); .build().toString();
} }
public static URI getMeUrl() { public static String getDefaultRepoName(String repositoryType) {
return RestUtil.createResourceUrl("me/"); 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() { public static URI getUsersUrl() {
return RestUtil.createResourceUrl("users/"); return RestUtil.createResourceUrl("users/");
} }
public static URI getUserUrl(String username) {
return getUsersUrl().resolve(username);
}
public static String createPasswordChangeJson(String oldPassword, String newPassword) { public static String createPasswordChangeJson(String oldPassword, String newPassword) {
return JSON_BUILDER return JSON_BUILDER
.add("oldPassword", oldPassword) .add("oldPassword", oldPassword)
@@ -225,4 +238,5 @@ public class TestData {
public static void main(String[] args) { public static void main(String[] args) {
cleanup(); cleanup();
} }
} }

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.0.7" "@scm-manager/ui-extensions": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.15" "@scm-manager/ui-bundler": "^0.0.17"
} }
} }

View File

@@ -35,9 +35,9 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
@@ -50,6 +50,7 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitSubModuleParser; import sonia.scm.repository.GitSubModuleParser;
@@ -103,10 +104,11 @@ public class GitBrowseCommand extends AbstractGitCommand
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request) public BrowserResult getBrowserResult(BrowseCommandRequest request)
throws IOException, RevisionNotFoundException { throws IOException, NotFoundException {
logger.debug("try to create browse result for {}", request); logger.debug("try to create browse result for {}", request);
BrowserResult result; BrowserResult result;
org.eclipse.jgit.lib.Repository repo = open(); org.eclipse.jgit.lib.Repository repo = open();
ObjectId revId; ObjectId revId;
@@ -121,7 +123,7 @@ public class GitBrowseCommand extends AbstractGitCommand
if (revId != null) if (revId != null)
{ {
result = getResult(repo, request, revId); result = new BrowserResult(revId.getName(), getEntry(repo, request, revId));
} }
else else
{ {
@@ -134,8 +136,7 @@ public class GitBrowseCommand extends AbstractGitCommand
logger.warn("coul not find head of repository, empty?"); logger.warn("coul not find head of repository, empty?");
} }
result = new BrowserResult(Constants.HEAD, null, null, result = new BrowserResult(Constants.HEAD, createEmtpyRoot());
Collections.EMPTY_LIST);
} }
return result; return result;
@@ -143,6 +144,14 @@ public class GitBrowseCommand extends AbstractGitCommand
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
private FileObject createEmtpyRoot() {
FileObject fileObject = new FileObject();
fileObject.setName("");
fileObject.setPath("");
fileObject.setDirectory(true);
return fileObject;
}
/** /**
* Method description * Method description
* *
@@ -158,11 +167,8 @@ public class GitBrowseCommand extends AbstractGitCommand
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
throws IOException, RevisionNotFoundException { throws IOException, RevisionNotFoundException {
FileObject file;
try FileObject file = new FileObject();
{
file = new FileObject();
String path = treeWalk.getPathString(); String path = treeWalk.getPathString();
@@ -193,7 +199,6 @@ public class GitBrowseCommand extends AbstractGitCommand
if (!file.isDirectory() &&!request.isDisableLastCommit()) if (!file.isDirectory() &&!request.isDisableLastCommit())
{ {
logger.trace("fetch last commit for {} at {}", path, revId.getName()); logger.trace("fetch last commit for {} at {}", path, revId.getName());
RevCommit commit = getLatestCommit(repo, revId, path); RevCommit commit = getLatestCommit(repo, revId, path);
if (commit != null) if (commit != null)
@@ -208,18 +213,6 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
} }
} }
}
catch (MissingObjectException ex)
{
file = null;
logger.error("could not fetch object for id {}", revId);
if (logger.isTraceEnabled())
{
logger.trace("could not fetch object", ex);
}
}
return file; return file;
} }
@@ -265,22 +258,19 @@ public class GitBrowseCommand extends AbstractGitCommand
return result; return result;
} }
private BrowserResult getResult(org.eclipse.jgit.lib.Repository repo, private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException, NotFoundException {
BrowseCommandRequest request, ObjectId revId)
throws IOException, RevisionNotFoundException {
BrowserResult result = null;
RevWalk revWalk = null; RevWalk revWalk = null;
TreeWalk treeWalk = null; TreeWalk treeWalk = null;
try FileObject result;
{
if (logger.isDebugEnabled()) try {
{
logger.debug("load repository browser for revision {}", revId.name()); logger.debug("load repository browser for revision {}", revId.name());
}
treeWalk = new TreeWalk(repo); treeWalk = new TreeWalk(repo);
treeWalk.setRecursive(request.isRecursive()); if (!isRootRequest(request)) {
treeWalk.setFilter(PathFilter.create(request.getPath()));
}
revWalk = new RevWalk(repo); revWalk = new RevWalk(repo);
RevTree tree = revWalk.parseTree(revId); RevTree tree = revWalk.parseTree(revId);
@@ -291,65 +281,20 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
else else
{ {
logger.error("could not find tree for {}", revId.name()); throw new IllegalStateException("could not find tree for " + revId.name());
} }
result = new BrowserResult(); if (isRootRequest(request)) {
result = createEmtpyRoot();
List<FileObject> files = Lists.newArrayList(); findChildren(result, repo, request, revId, treeWalk);
} else {
String path = request.getPath(); result = findFirstMatch(repo, request, revId, treeWalk);
if ( result.isDirectory() ) {
if (Util.isEmpty(path))
{
while (treeWalk.next())
{
FileObject fo = createFileObject(repo, request, revId, treeWalk);
if (fo != null)
{
files.add(fo);
}
}
}
else
{
String[] parts = path.split("/");
int current = 0;
int limit = parts.length;
while (treeWalk.next())
{
String name = treeWalk.getNameString();
if (current >= limit)
{
String p = treeWalk.getPathString();
if (p.split("/").length > limit)
{
FileObject fo = createFileObject(repo, request, revId, treeWalk);
if (fo != null)
{
files.add(fo);
}
}
}
else if (name.equalsIgnoreCase(parts[current]))
{
current++;
if (!request.isRecursive())
{
treeWalk.enterSubtree(); treeWalk.enterSubtree();
} findChildren(result, repo, request, revId, treeWalk);
}
} }
} }
result.setFiles(files);
result.setRevision(revId.getName());
} }
finally finally
{ {
@@ -360,6 +305,60 @@ public class GitBrowseCommand extends AbstractGitCommand
return result; return result;
} }
private boolean isRootRequest(BrowseCommandRequest request) {
return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath());
}
private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
List<FileObject> files = Lists.newArrayList();
while (treeWalk.next())
{
FileObject fileObject = createFileObject(repo, request, revId, treeWalk);
if (!fileObject.getPath().startsWith(parent.getPath())) {
parent.setChildren(files);
return fileObject;
}
files.add(fileObject);
if (request.isRecursive() && fileObject.isDirectory()) {
treeWalk.enterSubtree();
FileObject rc = findChildren(fileObject, repo, request, revId, treeWalk);
if (rc != null) {
files.add(rc);
}
}
}
parent.setChildren(files);
return null;
}
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException {
String[] pathElements = request.getPath().split("/");
int currentDepth = 0;
int limit = pathElements.length;
while (treeWalk.next()) {
String name = treeWalk.getNameString();
if (name.equalsIgnoreCase(pathElements[currentDepth])) {
currentDepth++;
if (currentDepth >= limit) {
return createFileObject(repo, request, revId, treeWalk);
} else {
treeWalk.enterSubtree();
}
}
}
throw new NotFoundException("file", request.getPath());
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Map<String, private Map<String,
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo, SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,

View File

@@ -2,15 +2,17 @@
import React from "react"; import React from "react";
import { repositories } from "@scm-manager/ui-components"; import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
type Props = { type Props = {
repository: Repository repository: Repository,
t: string => string
} }
class ProtocolInformation extends React.Component<Props> { class ProtocolInformation extends React.Component<Props> {
render() { render() {
const { repository } = this.props; const { repository, t } = this.props;
const href = repositories.getProtocolLinkByType(repository, "http"); const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) { if (!href) {
return null; return null;
@@ -18,11 +20,11 @@ class ProtocolInformation extends React.Component<Props> {
return ( return (
<div> <div>
<h4>Clone the repository</h4> <h4>{t("scm-git-plugin.information.clone")}</h4>
<pre> <pre>
<code>git clone {href}</code> <code>git clone {href}</code>
</pre> </pre>
<h4>Create a new repository</h4> <h4>{t("scm-git-plugin.information.create")}</h4>
<pre> <pre>
<code> <code>
git init {repository.name} git init {repository.name}
@@ -39,7 +41,7 @@ class ProtocolInformation extends React.Component<Props> {
<br /> <br />
</code> </code>
</pre> </pre>
<h4>Push an existing repository</h4> <h4>{t("scm-git-plugin.information.replace")}</h4>
<pre> <pre>
<code> <code>
git remote add origin {href} git remote add origin {href}
@@ -54,4 +56,4 @@ class ProtocolInformation extends React.Component<Props> {
} }
export default ProtocolInformation; export default translate("plugins")(ProtocolInformation);

View File

@@ -0,0 +1,9 @@
{
"scm-git-plugin": {
"information": {
"clone" : "Repository Klonen",
"create" : "Neue Repository erstellen",
"replace" : "Eine existierende Repository aktualisieren"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"scm-git-plugin": {
"information": {
"clone" : "Clone the repository",
"create" : "Create a new repository",
"replace" : "Push an existing repository"
}
}
}

View File

@@ -26,152 +26,114 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* *
* http://bitbucket.org/sdorra/scm-manager * http://bitbucket.org/sdorra/scm-manager
*
*/ */
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test; import org.junit.Test;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitConstants; import sonia.scm.repository.GitConstants;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
//~--- JDK imports ------------------------------------------------------------
/** /**
* Unit tests for {@link GitBrowseCommand}. * Unit tests for {@link GitBrowseCommand}.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitBrowseCommandTest extends AbstractGitCommandTestBase public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
{
/**
* Test browse command with default branch.
*/
@Test @Test
public void testDefaultBranch() throws IOException, RevisionNotFoundException { public void testGetFile() throws IOException, NotFoundException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("a.txt");
BrowserResult result = createCommand().getBrowserResult(request);
FileObject fileObject = result.getFile();
assertEquals("a.txt", fileObject.getName());
}
@Test
public void testDefaultDefaultBranch() throws IOException, NotFoundException {
// without default branch, the repository head should be used // without default branch, the repository head should be used
BrowserResult result = createCommand().getBrowserResult(new BrowseCommandRequest()); FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(result); assertNotNull(root);
List<FileObject> foList = result.getFiles(); Collection<FileObject> foList = root.getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());
assertEquals(4, foList.size());
assertEquals("a.txt", foList.get(0).getName()); assertThat(foList)
assertEquals("b.txt", foList.get(1).getName()); .extracting("name")
assertEquals("c", foList.get(2).getName()); .containsExactly("a.txt", "b.txt", "c", "f.txt");
assertEquals("f.txt", foList.get(3).getName());
// set default branch and fetch again
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
result = createCommand().getBrowserResult(new BrowseCommandRequest());
assertNotNull(result);
foList = result.getFiles();
assertNotNull(foList);
assertFalse(foList.isEmpty());
assertEquals(2, foList.size());
assertEquals("a.txt", foList.get(0).getName());
assertEquals("c", foList.get(1).getName());
} }
@Test @Test
public void testBrowse() throws IOException, RevisionNotFoundException { public void testExplicitDefaultBranch() throws IOException, NotFoundException {
BrowserResult result = repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
createCommand().getBrowserResult(new BrowseCommandRequest());
assertNotNull(result); FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(root);
List<FileObject> foList = result.getFiles(); Collection<FileObject> foList = root.getChildren();
assertThat(foList)
assertNotNull(foList); .extracting("name")
assertFalse(foList.isEmpty()); .containsExactly("a.txt", "c");
assertEquals(4, foList.size());
FileObject a = null;
FileObject c = null;
for (FileObject f : foList)
{
if ("a.txt".equals(f.getName()))
{
a = f;
}
else if ("c".equals(f.getName()))
{
c = f;
}
} }
assertNotNull(a); @Test
public void testBrowse() throws IOException, NotFoundException {
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(root);
Collection<FileObject> foList = root.getChildren();
FileObject a = findFile(foList, "a.txt");
FileObject c = findFile(foList, "c");
assertFalse(a.isDirectory()); assertFalse(a.isDirectory());
assertEquals("a.txt", a.getName()); assertEquals("a.txt", a.getName());
assertEquals("a.txt", a.getPath()); assertEquals("a.txt", a.getPath());
assertEquals("added new line for blame", a.getDescription()); assertEquals("added new line for blame", a.getDescription());
assertTrue(a.getLength() > 0); assertTrue(a.getLength() > 0);
checkDate(a.getLastModified()); checkDate(a.getLastModified());
assertNotNull(c);
assertTrue(c.isDirectory()); assertTrue(c.isDirectory());
assertEquals("c", c.getName()); assertEquals("c", c.getName());
assertEquals("c", c.getPath()); assertEquals("c", c.getPath());
} }
@Test @Test
public void testBrowseSubDirectory() throws IOException, RevisionNotFoundException { public void testBrowseSubDirectory() throws IOException, NotFoundException {
BrowseCommandRequest request = new BrowseCommandRequest(); BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("c"); request.setPath("c");
BrowserResult result = createCommand().getBrowserResult(request); FileObject root = createCommand().getBrowserResult(request).getFile();
assertNotNull(result); Collection<FileObject> foList = root.getChildren();
List<FileObject> foList = result.getFiles(); assertThat(foList).hasSize(2);
assertNotNull(foList); FileObject d = findFile(foList, "d.txt");
assertFalse(foList.isEmpty()); FileObject e = findFile(foList, "e.txt");
assertEquals(2, foList.size());
FileObject d = null;
FileObject e = null;
for (FileObject f : foList)
{
if ("d.txt".equals(f.getName()))
{
d = f;
}
else if ("e.txt".equals(f.getName()))
{
e = f;
}
}
assertNotNull(d);
assertFalse(d.isDirectory()); assertFalse(d.isDirectory());
assertEquals("d.txt", d.getName()); assertEquals("d.txt", d.getName());
assertEquals("c/d.txt", d.getPath()); assertEquals("c/d.txt", d.getPath());
assertEquals("added file d and e in folder c", d.getDescription()); assertEquals("added file d and e in folder c", d.getDescription());
assertTrue(d.getLength() > 0); assertTrue(d.getLength() > 0);
checkDate(d.getLastModified()); checkDate(d.getLastModified());
assertNotNull(e);
assertFalse(e.isDirectory()); assertFalse(e.isDirectory());
assertEquals("e.txt", e.getName()); assertEquals("e.txt", e.getName());
assertEquals("c/e.txt", e.getPath()); assertEquals("c/e.txt", e.getPath());
@@ -181,30 +143,35 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase
} }
@Test @Test
public void testRecusive() throws IOException, RevisionNotFoundException { public void testRecursive() throws IOException, NotFoundException {
BrowseCommandRequest request = new BrowseCommandRequest(); BrowseCommandRequest request = new BrowseCommandRequest();
request.setRecursive(true); request.setRecursive(true);
BrowserResult result = createCommand().getBrowserResult(request); FileObject root = createCommand().getBrowserResult(request).getFile();
assertNotNull(result); Collection<FileObject> foList = root.getChildren();
List<FileObject> foList = result.getFiles(); assertThat(foList)
.extracting("name")
.containsExactly("a.txt", "b.txt", "c", "f.txt");
assertNotNull(foList); FileObject c = findFile(foList, "c");
assertFalse(foList.isEmpty());
assertEquals(5, foList.size()); Collection<FileObject> cChildren = c.getChildren();
assertThat(cChildren)
.extracting("name")
.containsExactly("d.txt", "e.txt");
} }
/** private FileObject findFile(Collection<FileObject> foList, String name) {
* Method description return foList.stream()
* .filter(f -> name.equals(f.getName()))
* .findFirst()
* @return .orElseThrow(() -> new AssertionError("file " + name + " not found"));
*/ }
private GitBrowseCommand createCommand()
{ private GitBrowseCommand createCommand() {
return new GitBrowseCommand(createContext(), repository); return new GitBrowseCommand(createContext(), repository);
} }
} }

View File

@@ -707,9 +707,9 @@
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.15": "@scm-manager/ui-bundler@^0.0.17":
version "0.0.15" version "0.0.17"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -726,7 +726,6 @@
browserify-css "^0.14.0" browserify-css "^0.14.0"
colors "^1.3.1" colors "^1.3.1"
commander "^2.17.1" commander "^2.17.1"
connect-history-api-fallback "^1.5.0"
eslint "^5.4.0" eslint "^5.4.0"
eslint-config-react-app "^2.1.0" eslint-config-react-app "^2.1.0"
eslint-plugin-flowtype "^2.50.0" eslint-plugin-flowtype "^2.50.0"

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.0.7" "@scm-manager/ui-extensions": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.15" "@scm-manager/ui-bundler": "^0.0.17"
} }
} }

View File

@@ -35,8 +35,10 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.javahg.HgFileviewCommand; import sonia.scm.repository.spi.javahg.HgFileviewCommand;
@@ -45,6 +47,7 @@ import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
* Utilizes the mercurial fileview extension in order to support mercurial repository browsing.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@@ -94,16 +97,7 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
cmd.disableSubRepositoryDetection(); cmd.disableSubRepositoryDetection();
} }
BrowserResult result = new BrowserResult(); FileObject file = cmd.execute();
return new BrowserResult(MoreObjects.firstNonNull(request.getRevision(), "tip"), file);
result.setFiles(cmd.execute());
if (!Strings.isNullOrEmpty(request.getRevision())) {
result.setRevision(request.getRevision());
} else {
result.setRevision("tip");
}
return result;
} }
} }

View File

@@ -50,35 +50,31 @@ import sonia.scm.repository.SubRepository;
import java.io.IOException; import java.io.IOException;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List; import java.util.List;
/** /**
* Mercurial command to list files of a repository.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class HgFileviewCommand extends AbstractCommand public class HgFileviewCommand extends AbstractCommand
{ {
/** private boolean disableLastCommit = false;
* Constructs ...
* private HgFileviewCommand(Repository repository)
*
* @param repository
*/
public HgFileviewCommand(Repository repository)
{ {
super(repository); super(repository);
} }
//~--- methods --------------------------------------------------------------
/** /**
* Method description * Create command for the given repository.
* *
* @param repository repository
* *
* @param repository * @return fileview command
*
* @return
*/ */
public static HgFileviewCommand on(Repository repository) public static HgFileviewCommand on(Repository repository)
{ {
@@ -86,13 +82,11 @@ public class HgFileviewCommand extends AbstractCommand
} }
/** /**
* Method description * Disable last commit fetching for file objects.
* *
* * @return {@code this}
* @return
*/ */
public HgFileviewCommand disableLastCommit() public HgFileviewCommand disableLastCommit() {
{
disableLastCommit = true; disableLastCommit = true;
cmdAppend("-d"); cmdAppend("-d");
@@ -100,132 +94,128 @@ public class HgFileviewCommand extends AbstractCommand
} }
/** /**
* Method description * Disables sub repository detection
* *
* * @return {@code this}
* @return
*/ */
public HgFileviewCommand disableSubRepositoryDetection() public HgFileviewCommand disableSubRepositoryDetection() {
{
cmdAppend("-s"); cmdAppend("-s");
return this; return this;
} }
/** /**
* Method description * Start file object fetching at the given path.
* *
* *
* @return * @param path path to start fetching
* *
* @throws IOException * @return {@code this}
*/ */
public List<FileObject> execute() throws IOException public HgFileviewCommand path(String path) {
{
cmdAppend("-t");
List<FileObject> files = Lists.newArrayList();
HgInputStream stream = launchStream();
while (stream.peek() != -1)
{
FileObject file = null;
char type = (char) stream.read();
if (type == 'd')
{
file = readDirectory(stream);
}
else if (type == 'f')
{
file = readFile(stream);
}
else if (type == 's')
{
file = readSubRepository(stream);
}
if (file != null)
{
files.add(file);
}
}
return files;
}
/**
* Method description
*
*
* @param path
*
* @return
*/
public HgFileviewCommand path(String path)
{
cmdAppend("-p", path); cmdAppend("-p", path);
return this; return this;
} }
/** /**
* Method description * Fetch file objects recursive.
* *
* *
* @return * @return {@code this}
*/ */
public HgFileviewCommand recursive() public HgFileviewCommand recursive() {
{
cmdAppend("-c"); cmdAppend("-c");
return this; return this;
} }
/** /**
* Method description * Use given revision for file view.
* *
* @param revision revision id, hash, tag or branch
* *
* @param revision * @return {@code this}
*
* @return
*/ */
public HgFileviewCommand rev(String revision) public HgFileviewCommand rev(String revision) {
{
cmdAppend("-r", revision); cmdAppend("-r", revision);
return this; return this;
} }
//~--- get methods ----------------------------------------------------------
/** /**
* Method description * Executes the mercurial command and parses the output.
* *
* * @return file object
* @return
*/
@Override
public String getCommandName()
{
return HgFileviewExtension.NAME;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param stream
*
* @return
* *
* @throws IOException * @throws IOException
*/ */
private FileObject readDirectory(HgInputStream stream) throws IOException public FileObject execute() throws IOException
{ {
cmdAppend("-t");
Deque<FileObject> stack = new LinkedList<>();
HgInputStream stream = launchStream();
FileObject last = null;
while (stream.peek() != -1) {
FileObject file = read(stream);
while (!stack.isEmpty()) {
FileObject current = stack.peek();
if (isParent(current, file)) {
current.addChild(file);
break;
} else {
stack.pop();
}
}
if (file.isDirectory()) {
stack.push(file);
}
last = file;
}
if (stack.isEmpty()) {
// if the stack is empty, the requested path is probably a file
return last;
} else {
// if the stack is not empty, the requested path is a directory
return stack.getLast();
}
}
private FileObject read(HgInputStream stream) throws IOException {
char type = (char) stream.read();
FileObject file;
switch (type) {
case 'd':
file = readDirectory(stream);
break;
case 'f':
file = readFile(stream);
break;
case 's':
file = readSubRepository(stream);
break;
default:
throw new IOException("unknown file object type: " + type);
}
return file;
}
private boolean isParent(FileObject parent, FileObject child) {
String parentPath = parent.getPath();
if (parentPath.equals("")) {
return true;
}
return child.getParentPath().equals(parentPath);
}
private FileObject readDirectory(HgInputStream stream) throws IOException {
FileObject directory = new FileObject(); FileObject directory = new FileObject();
String path = removeTrailingSlash(stream.textUpTo('\0')); String path = removeTrailingSlash(stream.textUpTo('\0'));
@@ -236,18 +226,7 @@ public class HgFileviewCommand extends AbstractCommand
return directory; return directory;
} }
/** private FileObject readFile(HgInputStream stream) throws IOException {
* Method description
*
*
* @param stream
*
* @return
*
* @throws IOException
*/
private FileObject readFile(HgInputStream stream) throws IOException
{
FileObject file = new FileObject(); FileObject file = new FileObject();
String path = removeTrailingSlash(stream.textUpTo('\n')); String path = removeTrailingSlash(stream.textUpTo('\n'));
@@ -259,8 +238,7 @@ public class HgFileviewCommand extends AbstractCommand
DateTime timestamp = stream.dateTimeUpTo(' '); DateTime timestamp = stream.dateTimeUpTo(' ');
String description = stream.textUpTo('\0'); String description = stream.textUpTo('\0');
if (!disableLastCommit) if (!disableLastCommit) {
{
file.setLastModified(timestamp.getDate().getTime()); file.setLastModified(timestamp.getDate().getTime());
file.setDescription(description); file.setDescription(description);
} }
@@ -268,18 +246,7 @@ public class HgFileviewCommand extends AbstractCommand
return file; return file;
} }
/** private FileObject readSubRepository(HgInputStream stream) throws IOException {
* Method description
*
*
* @param stream
*
* @return
*
* @throws IOException
*/
private FileObject readSubRepository(HgInputStream stream) throws IOException
{
FileObject directory = new FileObject(); FileObject directory = new FileObject();
String path = removeTrailingSlash(stream.textUpTo('\n')); String path = removeTrailingSlash(stream.textUpTo('\n'));
@@ -292,8 +259,7 @@ public class HgFileviewCommand extends AbstractCommand
SubRepository subRepository = new SubRepository(url); SubRepository subRepository = new SubRepository(url);
if (!Strings.isNullOrEmpty(revision)) if (!Strings.isNullOrEmpty(revision)) {
{
subRepository.setRevision(revision); subRepository.setRevision(revision);
} }
@@ -302,48 +268,33 @@ public class HgFileviewCommand extends AbstractCommand
return directory; return directory;
} }
/** private String removeTrailingSlash(String path) {
* Method description if (path.endsWith("/")) {
*
*
* @param path
*
* @return
*/
private String removeTrailingSlash(String path)
{
if (path.endsWith("/"))
{
path = path.substring(0, path.length() - 1); path = path.substring(0, path.length() - 1);
} }
return path; return path;
} }
//~--- get methods ---------------------------------------------------------- private String getNameFromPath(String path) {
/**
* Method description
*
*
* @param path
*
* @return
*/
private String getNameFromPath(String path)
{
int index = path.lastIndexOf('/'); int index = path.lastIndexOf('/');
if (index > 0) if (index > 0) {
{
path = path.substring(index + 1); path = path.substring(index + 1);
} }
return path; return path;
} }
//~--- fields --------------------------------------------------------------- /**
* Returns the name of the mercurial command.
/** Field description */ *
private boolean disableLastCommit = false; * @return command name
*/
@Override
public String getCommandName()
{
return HgFileviewExtension.NAME;
}
} }

View File

@@ -2,26 +2,28 @@
import React from "react"; import React from "react";
import { repositories } from "@scm-manager/ui-components"; import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
type Props = { type Props = {
repository: Repository repository: Repository,
t: string => string
} }
class ProtocolInformation extends React.Component<Props> { class ProtocolInformation extends React.Component<Props> {
render() { render() {
const { repository } = this.props; const { repository, t } = this.props;
const href = repositories.getProtocolLinkByType(repository, "http"); const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) { if (!href) {
return null; return null;
} }
return ( return (
<div> <div>
<h4>Clone the repository</h4> <h4>{t("scm-hg-plugin.information.clone")}</h4>
<pre> <pre>
<code>hg clone {href}</code> <code>hg clone {href}</code>
</pre> </pre>
<h4>Create a new repository</h4> <h4>{t("scm-hg-plugin.information.create")}</h4>
<pre> <pre>
<code> <code>
hg init {repository.name} hg init {repository.name}
@@ -41,7 +43,7 @@ class ProtocolInformation extends React.Component<Props> {
<br /> <br />
</code> </code>
</pre> </pre>
<h4>Push an existing repository</h4> <h4>{t("scm-hg-plugin.information.replace")}</h4>
<pre> <pre>
<code> <code>
# add the repository url as default to your .hg/hgrc e.g: # add the repository url as default to your .hg/hgrc e.g:
@@ -59,4 +61,4 @@ class ProtocolInformation extends React.Component<Props> {
} }
export default ProtocolInformation; export default translate("plugins")(ProtocolInformation);

View File

@@ -0,0 +1,9 @@
{
"scm-hg-plugin": {
"information": {
"clone" : "Repository Klonen",
"create" : "Neue Repository erstellen",
"replace" : "Eine existierende Repository aktualisieren"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"scm-hg-plugin": {
"information": {
"clone" : "Clone the repository",
"create" : "Create a new repository",
"replace" : "Push an existing repository"
}
}
}

View File

@@ -32,61 +32,129 @@
Prints date, size and last message of files. Prints date, size and last message of files.
""" """
from collections import defaultdict
from mercurial import cmdutil,util from mercurial import cmdutil,util
cmdtable = {} cmdtable = {}
command = cmdutil.command(cmdtable) command = cmdutil.command(cmdtable)
FILE_MARKER = '<files>'
class File_Collector:
def __init__(self, recursive = False):
self.recursive = recursive
self.structure = defaultdict(dict, ((FILE_MARKER, []),))
def collect(self, paths, path = "", dir_only = False):
for p in paths:
if p.startswith(path):
self.attach(self.extract_name_without_parent(path, p), self.structure, dir_only)
def attach(self, branch, trunk, dir_only = False):
parts = branch.split('/', 1)
if len(parts) == 1: # branch is a file
if dir_only:
trunk[parts[0]] = defaultdict(dict, ((FILE_MARKER, []),))
else:
trunk[FILE_MARKER].append(parts[0])
else:
node, others = parts
if node not in trunk:
trunk[node] = defaultdict(dict, ((FILE_MARKER, []),))
if self.recursive:
self.attach(others, trunk[node], dir_only)
def extract_name_without_parent(self, parent, name_with_parent):
if len(parent) > 0:
name_without_parent = name_with_parent[len(parent):]
if name_without_parent.startswith("/"):
name_without_parent = name_without_parent[1:]
return name_without_parent
return name_with_parent
class File_Object:
def __init__(self, directory, path):
self.directory = directory
self.path = path
self.children = []
self.sub_repository = None
def get_name(self):
parts = self.path.split("/")
return parts[len(parts) - 1]
def get_parent(self):
idx = self.path.rfind("/")
if idx > 0:
return self.path[0:idx]
return ""
def add_child(self, child):
self.children.append(child)
def __getitem__(self, key):
return self.children[key]
def __len__(self):
return len(self.children)
def __repr__(self):
result = self.path
if self.directory:
result += "/"
return result
class File_Walker:
def __init__(self, sub_repositories, visitor):
self.visitor = visitor
self.sub_repositories = sub_repositories
def create_file(self, path):
return File_Object(False, path)
def create_directory(self, path):
directory = File_Object(True, path)
if path in self.sub_repositories:
directory.sub_repository = self.sub_repositories[path]
return directory
def visit_file(self, path):
file = self.create_file(path)
self.visit(file)
def visit_directory(self, path):
file = self.create_directory(path)
self.visit(file)
def visit(self, file):
self.visitor.visit(file)
def create_path(self, parent, path):
if len(parent) > 0:
return parent + "/" + path
return path
def walk(self, structure, parent = ""):
for key, value in structure.iteritems():
if key == FILE_MARKER:
if value:
for v in value:
self.visit_file(self.create_path(parent, v))
else:
self.visit_directory(self.create_path(parent, key))
if isinstance(value, dict):
self.walk(value, self.create_path(parent, key))
else:
self.visit_directory(self.create_path(parent, value))
class SubRepository: class SubRepository:
url = None url = None
revision = None revision = None
def removeTrailingSlash(path): def collect_sub_repositories(revCtx):
if path.endswith('/'):
path = path[0:-1]
return path
def appendTrailingSlash(path):
if not path.endswith('/'):
path += '/'
return path
def collectFiles(revCtx, path, files, directories, recursive):
length = 0
paths = []
mf = revCtx.manifest()
if path is "":
length = 1
for f in mf:
paths.append(f)
else:
length = len(path.split('/')) + 1
directory = path
if not directory.endswith('/'):
directory += '/'
for f in mf:
if f.startswith(directory):
paths.append(f)
if not recursive:
for p in paths:
parts = p.split('/')
depth = len(parts)
if depth is length:
file = revCtx[p]
files.append(file)
elif depth > length:
dirpath = ''
for i in range(0, length):
dirpath += parts[i] + '/'
if not dirpath in directories:
directories.append(dirpath)
else:
for p in paths:
files.append(revCtx[p])
def createSubRepositoryMap(revCtx):
subrepos = {} subrepos = {}
try: try:
hgsub = revCtx.filectx('.hgsub').data().split('\n') hgsub = revCtx.filectx('.hgsub').data().split('\n')
@@ -112,29 +180,74 @@ def createSubRepositoryMap(revCtx):
return subrepos return subrepos
def printSubRepository(ui, path, subrepository, transport): class File_Printer:
format = '%s %s %s\n'
if transport:
format = 's%s\n%s %s\0'
ui.write( format % (appendTrailingSlash(path), subrepository.revision, subrepository.url))
def printDirectory(ui, path, transport): def __init__(self, ui, repo, revCtx, disableLastCommit, transport):
format = '%s\n' self.ui = ui
if transport: self.repo = repo
format = 'd%s\0' self.revCtx = revCtx
ui.write( format % path) self.disableLastCommit = disableLastCommit
self.transport = transport
def printFile(ui, repo, file, disableLastCommit, transport): def print_directory(self, path):
format = '%s/\n'
if self.transport:
format = 'd%s/\0'
self.ui.write( format % path)
def print_file(self, path):
file = self.revCtx[path]
date = '0 0' date = '0 0'
description = 'n/a' description = 'n/a'
if not disableLastCommit: if not self.disableLastCommit:
linkrev = repo[file.linkrev()] linkrev = self.repo[file.linkrev()]
date = '%d %d' % util.parsedate(linkrev.date()) date = '%d %d' % util.parsedate(linkrev.date())
description = linkrev.description() description = linkrev.description()
format = '%s %i %s %s\n' format = '%s %i %s %s\n'
if transport: if self.transport:
format = 'f%s\n%i %s %s\0' format = 'f%s\n%i %s %s\0'
ui.write( format % (file.path(), file.size(), date, description) ) self.ui.write( format % (file.path(), file.size(), date, description) )
def print_sub_repository(self, path, subrepo):
format = '%s/ %s %s\n'
if self.transport:
format = 's%s/\n%s %s\0'
self.ui.write( format % (path, subrepo.revision, subrepo.url))
def visit(self, file):
if file.sub_repository:
self.print_sub_repository(file.path, file.sub_repository)
elif file.directory:
self.print_directory(file.path)
else:
self.print_file(file.path)
class File_Viewer:
def __init__(self, revCtx, visitor):
self.revCtx = revCtx
self.visitor = visitor
self.sub_repositories = {}
self.recursive = False
def remove_ending_slash(self, path):
if path.endswith("/"):
return path[:-1]
return path
def view(self, path = ""):
manifest = self.revCtx.manifest()
if len(path) > 0 and path in manifest:
self.visitor.visit(File_Object(False, path))
else:
p = self.remove_ending_slash(path)
collector = File_Collector(self.recursive)
walker = File_Walker(self.sub_repositories, self.visitor)
self.visitor.visit(File_Object(True, p))
collector.collect(manifest, p)
collector.collect(self.sub_repositories.keys(), p, True)
walker.walk(collector.structure, p)
@command('fileview', [ @command('fileview', [
('r', 'revision', 'tip', 'revision to print'), ('r', 'revision', 'tip', 'revision to print'),
@@ -145,23 +258,12 @@ def printFile(ui, repo, file, disableLastCommit, transport):
('t', 'transport', False, 'format the output for command server'), ('t', 'transport', False, 'format the output for command server'),
]) ])
def fileview(ui, repo, **opts): def fileview(ui, repo, **opts):
files = [] revCtx = repo[opts["revision"]]
directories = [] subrepos = {}
revision = opts['revision'] if not opts["disableSubRepositoryDetection"]:
if revision == None: subrepos = collect_sub_repositories(revCtx)
revision = 'tip' printer = File_Printer(ui, repo, revCtx, opts["disableLastCommit"], opts["transport"])
revCtx = repo[revision] viewer = File_Viewer(revCtx, printer)
path = opts['path'] viewer.recursive = opts["recursive"]
if path.endswith('/'): viewer.sub_repositories = subrepos
path = path[0:-1] viewer.view(opts["path"])
transport = opts['transport']
collectFiles(revCtx, path, files, directories, opts['recursive'])
if not opts['disableSubRepositoryDetection']:
subRepositories = createSubRepositoryMap(revCtx)
for k, v in subRepositories.iteritems():
if k.startswith(path):
printSubRepository(ui, k, v, transport)
for d in directories:
printDirectory(ui, d, transport)
for f in files:
printFile(ui, repo, f, opts['disableLastCommit'], transport)

View File

@@ -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()

View File

@@ -33,14 +33,12 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test; import org.junit.Test;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.Collection;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -48,18 +46,25 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class HgBrowseCommandTest extends AbstractHgCommandTestBase public class HgBrowseCommandTest extends AbstractHgCommandTestBase {
{
@Test
public void testBrowseWithFilePath() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("a.txt");
FileObject file = new HgBrowseCommand(cmdContext, repository).getBrowserResult(request).getFile();
assertEquals("a.txt", file.getName());
assertFalse(file.isDirectory());
assertTrue(file.getChildren().isEmpty());
}
@Test @Test
public void testBrowse() throws IOException { public void testBrowse() throws IOException {
List<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
FileObject a = getFileObject(foList, "a.txt"); FileObject a = getFileObject(foList, "a.txt");
FileObject c = getFileObject(foList, "c"); FileObject c = getFileObject(foList, "c");
@@ -85,7 +90,9 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
assertNotNull(result); assertNotNull(result);
List<FileObject> foList = result.getFiles(); FileObject c = result.getFile();
assertEquals("c", c.getName());
Collection<FileObject> foList = c.getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());
@@ -128,7 +135,7 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
request.setDisableLastCommit(true); request.setDisableLastCommit(true);
List<FileObject> foList = getRootFromTip(request); Collection<FileObject> foList = getRootFromTip(request);
FileObject a = getFileObject(foList, "a.txt"); FileObject a = getFileObject(foList, "a.txt");
@@ -147,11 +154,16 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
assertNotNull(result); assertNotNull(result);
List<FileObject> foList = result.getFiles(); FileObject root = result.getFile();
Collection<FileObject> foList = root.getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());
assertEquals(5, foList.size()); assertEquals(4, foList.size());
FileObject c = getFileObject(foList, "c");
assertTrue(c.isDirectory());
assertEquals(2, c.getChildren().size());
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -165,32 +177,22 @@ public class HgBrowseCommandTest extends AbstractHgCommandTestBase
* *
* @return * @return
*/ */
private FileObject getFileObject(List<FileObject> foList, String name) private FileObject getFileObject(Collection<FileObject> foList, String name)
{ {
FileObject a = null; return foList.stream()
.filter(f -> name.equals(f.getName()))
for (FileObject f : foList) .findFirst()
{ .orElseThrow(() -> new AssertionError("file " + name + " not found"));
if (name.equals(f.getName()))
{
a = f;
break;
}
} }
assertNotNull(a); private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
return a;
}
private List<FileObject> getRootFromTip(BrowseCommandRequest request) throws IOException {
BrowserResult result = new HgBrowseCommand(cmdContext, BrowserResult result = new HgBrowseCommand(cmdContext,
repository).getBrowserResult(request); repository).getBrowserResult(request);
assertNotNull(result); assertNotNull(result);
List<FileObject> foList = result.getFiles(); FileObject root = result.getFile();
Collection<FileObject> foList = root.getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());

View File

@@ -641,9 +641,9 @@
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.15": "@scm-manager/ui-bundler@^0.0.17":
version "0.0.15" version "0.0.17"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -660,7 +660,6 @@
browserify-css "^0.14.0" browserify-css "^0.14.0"
colors "^1.3.1" colors "^1.3.1"
commander "^2.17.1" commander "^2.17.1"
connect-history-api-fallback "^1.5.0"
eslint "^5.4.0" eslint "^5.4.0"
eslint-config-react-app "^2.1.0" eslint-config-react-app "^2.1.0"
eslint-plugin-flowtype "^2.50.0" eslint-plugin-flowtype "^2.50.0"

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.0.7" "@scm-manager/ui-extensions": "^0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.15" "@scm-manager/ui-bundler": "^0.0.17"
} }
} }

View File

@@ -35,6 +35,7 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -79,11 +80,11 @@ public class SvnBrowseCommand extends AbstractSvnCommand
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException { public BrowserResult getBrowserResult(BrowseCommandRequest request) throws RevisionNotFoundException {
String path = request.getPath(); String path = Strings.nullToEmpty(request.getPath());
long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision()); long revisionNumber = SvnUtil.getRevisionNumber(request.getRevision());
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("browser repository {} in path {} at revision {}", repository.getName(), path, revisionNumber); logger.debug("browser repository {} in path \"{}\" at revision {}", repository.getName(), path, revisionNumber);
} }
BrowserResult result = null; BrowserResult result = null;
@@ -91,34 +92,21 @@ public class SvnBrowseCommand extends AbstractSvnCommand
try try
{ {
SVNRepository svnRepository = open(); SVNRepository svnRepository = open();
Collection<SVNDirEntry> entries =
svnRepository.getDir(Util.nonNull(path), revisionNumber, null,
(Collection) null);
List<FileObject> children = Lists.newArrayList();
String basePath = createBasePath(path);
if (request.isRecursive())
{
browseRecursive(svnRepository, revisionNumber, request, children,
entries, basePath);
}
else
{
for (SVNDirEntry entry : entries)
{
children.add(createFileObject(request, svnRepository, revisionNumber,
entry, basePath));
}
}
if (revisionNumber == -1) { if (revisionNumber == -1) {
revisionNumber = svnRepository.getLatestRevision(); revisionNumber = svnRepository.getLatestRevision();
} }
result = new BrowserResult(); SVNDirEntry rootEntry = svnRepository.info(path, revisionNumber);
result.setRevision(String.valueOf(revisionNumber)); FileObject root = createFileObject(request, svnRepository, revisionNumber, rootEntry, path);
result.setFiles(children); root.setPath(path);
if (root.isDirectory()) {
traverse(svnRepository, revisionNumber, request, root, createBasePath(path));
}
result = new BrowserResult(String.valueOf(revisionNumber), root);
} }
catch (SVNException ex) catch (SVNException ex)
{ {
@@ -130,52 +118,24 @@ public class SvnBrowseCommand extends AbstractSvnCommand
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param svnRepository
* @param revisionNumber
* @param request
* @param children
* @param entries
* @param basePath
*
* @throws SVNException
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void browseRecursive(SVNRepository svnRepository, private void traverse(SVNRepository svnRepository, long revisionNumber, BrowseCommandRequest request,
long revisionNumber, BrowseCommandRequest request, FileObject parent, String basePath)
List<FileObject> children, Collection<SVNDirEntry> entries, String basePath)
throws SVNException throws SVNException
{ {
Collection<SVNDirEntry> entries = svnRepository.getDir(parent.getPath(), revisionNumber, null, (Collection) null);
for (SVNDirEntry entry : entries) for (SVNDirEntry entry : entries)
{ {
FileObject fo = createFileObject(request, svnRepository, revisionNumber, FileObject child = createFileObject(request, svnRepository, revisionNumber, entry, basePath);
entry, basePath);
children.add(fo); parent.addChild(child);
if (fo.isDirectory()) if (child.isDirectory() && request.isRecursive()) {
{ traverse(svnRepository, revisionNumber, request, child, createBasePath(child.getPath()));
Collection<SVNDirEntry> subEntries =
svnRepository.getDir(Util.nonNull(fo.getPath()), revisionNumber,
null, (Collection) null);
browseRecursive(svnRepository, revisionNumber, request, children,
subEntries, createBasePath(fo.getPath()));
} }
} }
} }
/**
* Method description
*
*
* @param path
*
* @return
*/
private String createBasePath(String path) private String createBasePath(String path)
{ {
String basePath = Util.EMPTY_STRING; String basePath = Util.EMPTY_STRING;
@@ -193,20 +153,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
return basePath; return basePath;
} }
/**
* Method description
*
*
*
*
* @param request
* @param repository
* @param revision
* @param entry
* @param path
*
* @return
*/
private FileObject createFileObject(BrowseCommandRequest request, private FileObject createFileObject(BrowseCommandRequest request,
SVNRepository repository, long revision, SVNDirEntry entry, String path) SVNRepository repository, long revision, SVNDirEntry entry, String path)
{ {
@@ -237,15 +183,6 @@ public class SvnBrowseCommand extends AbstractSvnCommand
return fileObject; return fileObject;
} }
/**
* Method description
*
*
* @param repository
* @param revision
* @param entry
* @param fileObject
*/
private void fetchExternalsProperty(SVNRepository repository, long revision, private void fetchExternalsProperty(SVNRepository repository, long revision,
SVNDirEntry entry, FileObject fileObject) SVNDirEntry entry, FileObject fileObject)
{ {

View File

@@ -2,22 +2,24 @@
import React from "react"; import React from "react";
import { repositories } from "@scm-manager/ui-components"; import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
type Props = { type Props = {
repository: Repository repository: Repository,
t: string => string
} }
class ProtocolInformation extends React.Component<Props> { class ProtocolInformation extends React.Component<Props> {
render() { render() {
const { repository } = this.props; const { repository, t } = this.props;
const href = repositories.getProtocolLinkByType(repository, "http"); const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) { if (!href) {
return null; return null;
} }
return ( return (
<div> <div>
<h4>Checkout the repository</h4> <h4>{t("scm-svn-plugin.information.checkout")}</h4>
<pre> <pre>
<code>svn checkout {href}</code> <code>svn checkout {href}</code>
</pre> </pre>
@@ -27,4 +29,4 @@ class ProtocolInformation extends React.Component<Props> {
} }
export default ProtocolInformation; export default translate("plugins")(ProtocolInformation);

View File

@@ -0,0 +1,7 @@
{
"scm-svn-plugin": {
"information": {
"checkout" : "Repository auschecken"
}
}
}

View File

@@ -0,0 +1,7 @@
{
"scm-svn-plugin": {
"information": {
"checkout" : "Checkout repository"
}
}
}

View File

@@ -33,15 +33,13 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test; import org.junit.Test;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.Collection;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -49,8 +47,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -58,9 +54,19 @@ import static org.junit.Assert.assertTrue;
public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
{ {
@Test
public void testBrowseWithFilePath() throws RevisionNotFoundException {
BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("a.txt");
FileObject file = createCommand().getBrowserResult(request).getFile();
assertEquals("a.txt", file.getName());
assertFalse(file.isDirectory());
assertTrue(file.getChildren().isEmpty());
}
@Test @Test
public void testBrowse() throws RevisionNotFoundException { public void testBrowse() throws RevisionNotFoundException {
List<FileObject> foList = getRootFromTip(new BrowseCommandRequest()); Collection<FileObject> foList = getRootFromTip(new BrowseCommandRequest());
FileObject a = getFileObject(foList, "a.txt"); FileObject a = getFileObject(foList, "a.txt");
FileObject c = getFileObject(foList, "c"); FileObject c = getFileObject(foList, "c");
@@ -92,7 +98,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
assertNotNull(result); assertNotNull(result);
List<FileObject> foList = result.getFiles(); Collection<FileObject> foList = result.getFile().getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());
@@ -135,7 +141,7 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
request.setDisableLastCommit(true); request.setDisableLastCommit(true);
List<FileObject> foList = getRootFromTip(request); Collection<FileObject> foList = getRootFromTip(request);
FileObject a = getFileObject(foList, "a.txt"); FileObject a = getFileObject(foList, "a.txt");
@@ -151,15 +157,16 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
assertNotNull(result); assertNotNull(result);
List<FileObject> foList = result.getFiles(); Collection<FileObject> foList = result.getFile().getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());
assertEquals(4, foList.size()); assertEquals(2, foList.size());
for ( FileObject fo : foList ){ FileObject c = getFileObject(foList, "c");
System.out.println(fo); assertEquals("c", c.getName());
} assertTrue(c.isDirectory());
assertEquals(2, c.getChildren().size());
} }
/** /**
@@ -184,31 +191,20 @@ public class SvnBrowseCommandTest extends AbstractSvnCommandTestBase
* *
* @return * @return
*/ */
private FileObject getFileObject(List<FileObject> foList, String name) private FileObject getFileObject(Collection<FileObject> foList, String name)
{ {
FileObject a = null; return foList.stream()
.filter(f -> name.equals(f.getName()))
for (FileObject f : foList) .findFirst()
{ .orElseThrow(() -> new AssertionError("file " + name + " not found"));
if (name.equals(f.getName()))
{
a = f;
break;
}
} }
assertNotNull(a); private Collection<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
return a;
}
private List<FileObject> getRootFromTip(BrowseCommandRequest request) throws RevisionNotFoundException {
BrowserResult result = createCommand().getBrowserResult(request); BrowserResult result = createCommand().getBrowserResult(request);
assertNotNull(result); assertNotNull(result);
List<FileObject> foList = result.getFiles(); Collection<FileObject> foList = result.getFile().getChildren();
assertNotNull(foList); assertNotNull(foList);
assertFalse(foList.isEmpty()); assertFalse(foList.isEmpty());

View File

@@ -641,9 +641,9 @@
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.15": "@scm-manager/ui-bundler@^0.0.17":
version "0.0.15" version "0.0.17"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -660,7 +660,6 @@
browserify-css "^0.14.0" browserify-css "^0.14.0"
colors "^1.3.1" colors "^1.3.1"
commander "^2.17.1" commander "^2.17.1"
connect-history-api-fallback "^1.5.0"
eslint "^5.4.0" eslint "^5.4.0"
eslint-config-react-app "^2.1.0" eslint-config-react-app "^2.1.0"
eslint-plugin-flowtype "^2.50.0" eslint-plugin-flowtype "^2.50.0"

View File

@@ -196,7 +196,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> {
} }
@Test(expected = NotFoundException.class) @Test(expected = NotFoundException.class)
public void testModifyNotExisting() throws NotFoundException, ConcurrentModificationException { public void testModifyNotExisting() {
manager.modify(UserTestData.createZaphod()); manager.modify(UserTestData.createZaphod());
} }
@@ -249,7 +249,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> {
} }
@Test(expected = NotFoundException.class) @Test(expected = NotFoundException.class)
public void testRefreshNotFound() throws NotFoundException { public void testRefreshNotFound(){
manager.refresh(UserTestData.createDent()); manager.refresh(UserTestData.createDent());
} }

View File

@@ -12,20 +12,21 @@
"eslint-fix": "eslint src --fix" "eslint-fix": "eslint src --fix"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.15", "@scm-manager/ui-bundler": "^0.0.17",
"create-index": "^2.3.0", "create-index": "^2.3.0",
"enzyme": "^3.5.0", "enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1", "enzyme-adapter-react-16": "^1.3.1",
"flow-bin": "^0.79.1", "flow-bin": "^0.79.1",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"jest": "^23.5.0", "jest": "^23.5.0",
"raf": "^3.4.0" "raf": "^3.4.0",
"react-router-enzyme-context": "^1.2.0"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"moment": "^2.22.2", "moment": "^2.22.2",
"react": "^16.4.2", "react": "^16.5.2",
"react-dom": "^16.4.2", "react-dom": "^16.5.2",
"react-i18next": "^7.11.0", "react-i18next": "^7.11.0",
"react-jss": "^8.6.1", "react-jss": "^8.6.1",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",

View File

@@ -9,9 +9,18 @@ type Props = {
}; };
class Image extends React.Component<Props> { class Image extends React.Component<Props> {
createImageSrc = () => {
const { src } = this.props;
if (src.startsWith("http")) {
return src;
}
return withContextPath(src);
};
render() { render() {
const { src, alt, className } = this.props; const { alt, className } = this.props;
return <img className={className} src={withContextPath(src)} alt={alt} />; return <img className={className} src={this.createImageSrc()} alt={alt} />;
} }
} }

View 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">&hellip;</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);

View File

@@ -18,8 +18,10 @@ class Paginator extends React.Component<Props> {
createAction = (linkType: string) => () => { createAction = (linkType: string) => () => {
const { collection, onPageChange } = this.props; const { collection, onPageChange } = this.props;
if (onPageChange) { if (onPageChange) {
const link = collection._links[linkType].href; const link = collection._links[linkType];
onPageChange(link); if (link && link.href) {
onPageChange(link.href);
}
} }
}; };

View File

@@ -3,10 +3,13 @@ import React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import "./tests/enzyme"; import "./tests/enzyme";
import "./tests/i18n"; import "./tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import Paginator from "./Paginator"; import Paginator from "./Paginator";
describe("paginator rendering tests", () => { describe("paginator rendering tests", () => {
const options = new ReactRouterEnzymeContext();
const dummyLink = { const dummyLink = {
href: "https://dummy" href: "https://dummy"
}; };
@@ -18,7 +21,10 @@ describe("paginator rendering tests", () => {
_links: {} _links: {}
}; };
const paginator = shallow(<Paginator collection={collection} />); const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(7); expect(buttons.length).toBe(7);
for (let button of buttons) { 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"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(5); 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"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(6); 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"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(5); 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"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(6); 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"); const buttons = paginator.find("Button");
expect(buttons.length).toBe(7); expect(buttons.length).toBe(7);
@@ -244,7 +265,8 @@ describe("paginator rendering tests", () => {
}; };
const paginator = mount( const paginator = mount(
<Paginator collection={collection} onPageChange={callMe} /> <Paginator collection={collection} onPageChange={callMe} />,
options.get()
); );
paginator.find("Button.pagination-previous").simulate("click"); paginator.find("Button.pagination-previous").simulate("click");

View File

@@ -1,7 +1,7 @@
//@flow //@flow
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Link } from "react-router-dom"; import { withRouter } from "react-router-dom";
export type ButtonProps = { export type ButtonProps = {
label: string, label: string,
@@ -16,7 +16,10 @@ export type ButtonProps = {
type Props = ButtonProps & { type Props = ButtonProps & {
type: string, type: string,
color: string color: string,
// context prop
history: any
}; };
class Button extends React.Component<Props> { class Button extends React.Component<Props> {
@@ -25,14 +28,22 @@ class Button extends React.Component<Props> {
color: "default" color: "default"
}; };
renderButton = () => { onClick = (event: Event) => {
const { action, link, history } = this.props;
if (action) {
action(event);
} else if (link) {
history.push(link);
}
};
render() {
const { const {
label, label,
loading, loading,
disabled, disabled,
type, type,
color, color,
action,
fullWidth, fullWidth,
className className
} = this.props; } = this.props;
@@ -42,7 +53,7 @@ class Button extends React.Component<Props> {
<button <button
type={type} type={type}
disabled={disabled} disabled={disabled}
onClick={action ? action : (event: Event) => {}} onClick={this.onClick}
className={classNames( className={classNames(
"button", "button",
"is-" + color, "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);

View File

@@ -15,9 +15,11 @@ export { default as Logo } from "./Logo.js";
export { default as MailLink } from "./MailLink.js"; export { default as MailLink } from "./MailLink.js";
export { default as Notification } from "./Notification.js"; export { default as Notification } from "./Notification.js";
export { default as Paginator } from "./Paginator.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 ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help.js"; export { default as Help } from "./Help.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js"; export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
export { getPageFromMatch } from "./urls";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js"; export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";

View File

@@ -7,7 +7,8 @@ import { Route, Link } from "react-router-dom";
type Props = { type Props = {
to: string, to: string,
label: string, label: string,
activeOnlyWhenExact?: boolean activeOnlyWhenExact?: boolean,
activeWhenMatch?: (route: any) => boolean
}; };
class NavLink extends React.Component<Props> { class NavLink extends React.Component<Props> {
@@ -15,11 +16,17 @@ class NavLink extends React.Component<Props> {
activeOnlyWhenExact: true activeOnlyWhenExact: true
}; };
isActive(route: any) {
const { activeWhenMatch } = this.props;
return route.match || (activeWhenMatch && activeWhenMatch(route));
}
renderLink = (route: any) => { renderLink = (route: any) => {
const { to, label } = this.props; const { to, label } = this.props;
return ( return (
<li> <li>
<Link className={route.match ? "is-active" : ""} to={to}> <Link className={this.isActive(route) ? "is-active" : ""} to={to}>
{label} {label}
</Link> </Link>
</li> </li>

View File

@@ -4,38 +4,58 @@ import { translate } from "react-i18next";
import PrimaryNavigationLink from "./PrimaryNavigationLink"; import PrimaryNavigationLink from "./PrimaryNavigationLink";
type Props = { type Props = {
t: string => string t: string => string,
repositoriesLink: string,
usersLink: string,
groupsLink: string,
configLink: string,
logoutLink: string
}; };
class PrimaryNavigation extends React.Component<Props> { class PrimaryNavigation extends React.Component<Props> {
render() { render() {
const { t } = this.props; const { t, repositoriesLink, usersLink, groupsLink, configLink, logoutLink } = this.props;
return (
<nav className="tabs is-boxed"> const links = [
<ul> repositoriesLink ? (
<PrimaryNavigationLink <PrimaryNavigationLink
to="/repos" to="/repos"
match="/(repo|repos)" match="/(repo|repos)"
label={t("primary-navigation.repositories")} label={t("primary-navigation.repositories")}
/> key={"repositoriesLink"}
/>): null,
usersLink ? (
<PrimaryNavigationLink <PrimaryNavigationLink
to="/users" to="/users"
match="/(user|users)" match="/(user|users)"
label={t("primary-navigation.users")} label={t("primary-navigation.users")}
/> key={"usersLink"}
/>) : null,
groupsLink ? (
<PrimaryNavigationLink <PrimaryNavigationLink
to="/groups" to="/groups"
match="/(group|groups)" match="/(group|groups)"
label={t("primary-navigation.groups")} label={t("primary-navigation.groups")}
/> key={"groupsLink"}
/>) : null,
configLink ? (
<PrimaryNavigationLink <PrimaryNavigationLink
to="/config" to="/config"
label={t("primary-navigation.config")} label={t("primary-navigation.config")}
/> key={"configLink"}
/>) : null,
logoutLink ? (
<PrimaryNavigationLink <PrimaryNavigationLink
to="/logout" to="/logout"
label={t("primary-navigation.logout")} label={t("primary-navigation.logout")}
/> key={"logoutLink"}
/>) : null
];
return (
<nav className="tabs is-boxed">
<ul>
{links}
</ul> </ul>
</nav> </nav>
); );

View File

@@ -1,7 +1,7 @@
// @flow // @flow
import type { Repository } from "@scm-manager/ui-types"; import type { Repository } from "@scm-manager/ui-types";
import { getProtocolLinkByType, getTypePredicate } from "./repositories"; import { getProtocolLinkByType } from "./repositories";
describe("getProtocolLinkByType tests", () => { describe("getProtocolLinkByType tests", () => {

View File

@@ -4,3 +4,26 @@ export const contextPath = window.ctxPath || "";
export function withContextPath(path: string) { export function withContextPath(path: string) {
return contextPath + path; 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;
}

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"check": "flow check" "check": "flow check"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.15" "@scm-manager/ui-bundler": "^0.0.17"
}, },
"browserify": { "browserify": {
"transform": [ "transform": [

View File

@@ -0,0 +1,8 @@
//@flow
import type {Links} from "./hal";
export type Branch = {
name: string,
revision: string,
_links: Links
}

View 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
}

View File

@@ -0,0 +1,7 @@
//@flow
import type { Links } from "./hal";
export type IndexResources = {
version: string,
_links: Links
};

View File

@@ -1,14 +1,11 @@
//@flow //@flow
import type { Links } from "./hal"; import type { Links } from "./hal";
export type Permission = { export type Permission = PermissionCreateEntry & {
name: string, _links: Links
type: string,
groupPermission: boolean,
_links?: Links
}; };
export type PermissionEntry = { export type PermissionCreateEntry = {
name: string, name: string,
type: string, type: string,
groupPermission: boolean groupPermission: boolean

View 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[]
}
};

View File

@@ -0,0 +1,8 @@
//@flow
import type { Links } from "./hal";
export type Tag = {
name: string,
revision: string,
_links: Links
}

View File

@@ -4,10 +4,14 @@ export type Link = {
name?: string name?: string
}; };
export type Links = { [string]: Link | Link[] }; type LinkValue = Link | Link[];
// TODO use LinkValue
export type Links = { [string]: any };
export type Collection = { export type Collection = {
_embedded: Object, _embedded: Object,
// $FlowFixMe
_links: Links _links: Links
}; };

View File

@@ -9,6 +9,16 @@ export type { Group, Member } from "./Group";
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories"; export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; 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 { 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";

View File

@@ -707,9 +707,9 @@
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.15": "@scm-manager/ui-bundler@^0.0.17":
version "0.0.15" version "0.0.17"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -726,7 +726,6 @@
browserify-css "^0.14.0" browserify-css "^0.14.0"
colors "^1.3.1" colors "^1.3.1"
commander "^2.17.1" commander "^2.17.1"
connect-history-api-fallback "^1.5.0"
eslint "^5.4.0" eslint "^5.4.0"
eslint-config-react-app "^2.1.0" eslint-config-react-app "^2.1.0"
eslint-plugin-flowtype "^2.50.0" eslint-plugin-flowtype "^2.50.0"

View File

@@ -16,9 +16,8 @@
"i18next-browser-languagedetector": "^2.2.2", "i18next-browser-languagedetector": "^2.2.2",
"i18next-fetch-backend": "^0.1.0", "i18next-fetch-backend": "^0.1.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"node-sass": "^4.9.3", "react": "^16.5.2",
"react": "^16.4.2", "react-dom": "^16.5.2",
"react-dom": "^16.4.2",
"react-i18next": "^7.9.0", "react-i18next": "^7.9.0",
"react-jss": "^8.6.0", "react-jss": "^8.6.0",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
@@ -44,13 +43,14 @@
"pre-commit": "jest && flow && eslint src" "pre-commit": "jest && flow && eslint src"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.15", "@scm-manager/ui-bundler": "^0.0.17",
"copyfiles": "^2.0.0", "copyfiles": "^2.0.0",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1", "enzyme-adapter-react-16": "^1.1.1",
"fetch-mock": "^6.5.0", "fetch-mock": "^6.5.0",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"jest": "^23.5.0", "jest": "^23.5.0",
"node-sass": "^4.9.3",
"node-sass-chokidar": "^1.3.0", "node-sass-chokidar": "^1.3.0",
"npm-run-all": "^4.1.3", "npm-run-all": "^4.1.3",
"prettier": "^1.13.7", "prettier": "^1.13.7",

View File

@@ -22,8 +22,10 @@
"actions-label": "Actions", "actions-label": "Actions",
"back-label": "Back", "back-label": "Back",
"navigation-label": "Navigation", "navigation-label": "Navigation",
"history": "Commits",
"information": "Information", "information": "Information",
"permissions": "Permissions" "permissions": "Permissions",
"sources": "Sources"
}, },
"create": { "create": {
"title": "Create Repository", "title": "Create Repository",
@@ -44,6 +46,32 @@
"cancel": "No" "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": { "permission": {
"error-title": "Error", "error-title": "Error",
"error-subtitle": "Unknown permissions error", "error-subtitle": "Unknown permissions error",
@@ -67,6 +95,11 @@
"add-permission-heading": "Add new Permission", "add-permission-heading": "Add new Permission",
"submit-button": "Submit", "submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!" "name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
},
"help": {
"groupPermissionHelpText": "States if a permission is a group permission.",
"nameHelpText": "Manage permissions for a specific user or group",
"typeHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions"
} }
}, },
"help": { "help": {

View File

@@ -14,18 +14,20 @@ import {
modifyConfigReset modifyConfigReset
} from "../modules/config"; } from "../modules/config";
import { connect } from "react-redux"; import { connect } from "react-redux";
import type { Config } from "@scm-manager/ui-types"; import type { Config, Link } from "@scm-manager/ui-types";
import ConfigForm from "../components/form/ConfigForm"; import ConfigForm from "../components/form/ConfigForm";
import { getConfigLink } from "../../modules/indexResource";
type Props = { type Props = {
loading: boolean, loading: boolean,
error: Error, error: Error,
config: Config, config: Config,
configUpdatePermission: boolean, configUpdatePermission: boolean,
configLink: string,
// dispatch functions // dispatch functions
modifyConfig: (config: Config, callback?: () => void) => void, modifyConfig: (config: Config, callback?: () => void) => void,
fetchConfig: void => void, fetchConfig: (link: string) => void,
configReset: void => void, configReset: void => void,
// context objects // context objects
@@ -35,7 +37,7 @@ type Props = {
class GlobalConfig extends React.Component<Props> { class GlobalConfig extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.configReset(); this.props.configReset();
this.props.fetchConfig(); this.props.fetchConfig(this.props.configLink);
} }
modifyConfig = (config: Config) => { modifyConfig = (config: Config) => {
@@ -75,8 +77,8 @@ class GlobalConfig extends React.Component<Props> {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchConfig: () => { fetchConfig: (link: string) => {
dispatch(fetchConfig()); dispatch(fetchConfig(link));
}, },
modifyConfig: (config: Config, callback?: () => void) => { modifyConfig: (config: Config, callback?: () => void) => {
dispatch(modifyConfig(config, callback)); dispatch(modifyConfig(config, callback));
@@ -92,12 +94,14 @@ const mapStateToProps = state => {
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state); const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
const config = getConfig(state); const config = getConfig(state);
const configUpdatePermission = getConfigUpdatePermission(state); const configUpdatePermission = getConfigUpdatePermission(state);
const configLink = getConfigLink(state);
return { return {
loading, loading,
error, error,
config, config,
configUpdatePermission configUpdatePermission,
configLink
}; };
}; };

View File

@@ -18,15 +18,14 @@ export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`;
export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`; export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`;
export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`; export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
const CONFIG_URL = "config";
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2"; const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
//fetch config //fetch config
export function fetchConfig() { export function fetchConfig(link: string) {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchConfigPending()); dispatch(fetchConfigPending());
return apiClient return apiClient
.get(CONFIG_URL) .get(link)
.then(response => { .then(response => {
return response.json(); return response.json();
}) })

View File

@@ -22,8 +22,10 @@ import reducer, {
getConfig, getConfig,
getConfigUpdatePermission getConfigUpdatePermission
} from "./config"; } from "./config";
import { getConfigLink } from "../../modules/indexResource";
const CONFIG_URL = "/api/v2/config"; const CONFIG_URL = "/config";
const URL = "/api/v2" + CONFIG_URL;
const error = new Error("You have an error!"); const error = new Error("You have an error!");
@@ -103,7 +105,7 @@ describe("config fetch()", () => {
}); });
it("should successfully fetch config", () => { it("should successfully fetch config", () => {
fetchMock.getOnce(CONFIG_URL, response); fetchMock.getOnce(URL, response);
const expectedActions = [ const expectedActions = [
{ type: FETCH_CONFIG_PENDING }, { type: FETCH_CONFIG_PENDING },
@@ -113,20 +115,36 @@ describe("config fetch()", () => {
} }
]; ];
const store = mockStore({}); const store = mockStore({
indexResources: {
links: {
config: {
href: CONFIG_URL
}
}
}
});
return store.dispatch(fetchConfig()).then(() => { return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
it("should fail getting config on HTTP 500", () => { it("should fail getting config on HTTP 500", () => {
fetchMock.getOnce(CONFIG_URL, { fetchMock.getOnce(URL, {
status: 500 status: 500
}); });
const store = mockStore({}); const store = mockStore({
return store.dispatch(fetchConfig()).then(() => { indexResources: {
links: {
config: {
href: CONFIG_URL
}
}
}
});
return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING); expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE); expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);

View File

@@ -19,16 +19,33 @@ import {
Footer, Footer,
Header Header
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import type { Me } from "@scm-manager/ui-types"; import type { Me, Link } from "@scm-manager/ui-types";
import {
fetchIndexResources,
getConfigLink,
getFetchIndexResourcesFailure,
getGroupsLink,
getLogoutLink,
getMeLink,
getRepositoriesLink,
getUsersLink,
isFetchIndexResourcesPending
} from "../modules/indexResource";
type Props = { type Props = {
me: Me, me: Me,
authenticated: boolean, authenticated: boolean,
error: Error, error: Error,
loading: boolean, loading: boolean,
repositoriesLink: string,
usersLink: string,
groupsLink: string,
configLink: string,
logoutLink: string,
meLink: string,
// dispatcher functions // dispatcher functions
fetchMe: () => void, fetchMe: (link: string) => void,
// context props // context props
t: string => string t: string => string
@@ -36,14 +53,37 @@ type Props = {
class App extends Component<Props> { class App extends Component<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchMe(); if (this.props.meLink) {
this.props.fetchMe(this.props.meLink);
}
} }
render() { render() {
const { me, loading, error, authenticated, t } = this.props; const {
me,
loading,
error,
authenticated,
t,
repositoriesLink,
usersLink,
groupsLink,
configLink,
logoutLink
} = this.props;
let content; let content;
const navigation = authenticated ? <PrimaryNavigation /> : ""; const navigation = authenticated ? (
<PrimaryNavigation
repositoriesLink={repositoriesLink}
usersLink={usersLink}
groupsLink={groupsLink}
configLink={configLink}
logoutLink={logoutLink}
/>
) : (
""
);
if (loading) { if (loading) {
content = <Loading />; content = <Loading />;
@@ -70,20 +110,34 @@ class App extends Component<Props> {
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
fetchMe: () => dispatch(fetchMe()) fetchMe: (link: string) => dispatch(fetchMe(link))
}; };
}; };
const mapStateToProps = state => { const mapStateToProps = state => {
const authenticated = isAuthenticated(state); const authenticated = isAuthenticated(state);
const me = getMe(state); const me = getMe(state);
const loading = isFetchMePending(state); const loading =
const error = getFetchMeFailure(state); isFetchMePending(state) || isFetchIndexResourcesPending(state);
const error =
getFetchMeFailure(state) || getFetchIndexResourcesFailure(state);
const repositoriesLink = getRepositoriesLink(state);
const usersLink = getUsersLink(state);
const groupsLink = getGroupsLink(state);
const configLink = getConfigLink(state);
const logoutLink = getLogoutLink(state);
const meLink = getMeLink(state);
return { return {
authenticated, authenticated,
me, me,
loading, loading,
error error,
repositoriesLink,
usersLink,
groupsLink,
configLink,
logoutLink,
meLink
}; };
}; };

View 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))
);

View File

@@ -18,6 +18,7 @@ import {
Image Image
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import classNames from "classnames"; import classNames from "classnames";
import { getLoginLink } from "../modules/indexResource";
const styles = { const styles = {
avatar: { avatar: {
@@ -41,9 +42,10 @@ type Props = {
authenticated: boolean, authenticated: boolean,
loading: boolean, loading: boolean,
error: Error, error: Error,
link: string,
// dispatcher props // dispatcher props
login: (username: string, password: string) => void, login: (link: string, username: string, password: string) => void,
// context props // context props
t: string => string, t: string => string,
@@ -74,7 +76,11 @@ class Login extends React.Component<Props, State> {
handleSubmit = (event: Event) => { handleSubmit = (event: Event) => {
event.preventDefault(); event.preventDefault();
if (this.isValid()) { if (this.isValid()) {
this.props.login(this.state.username, this.state.password); this.props.login(
this.props.link,
this.state.username,
this.state.password
);
} }
}; };
@@ -145,17 +151,19 @@ const mapStateToProps = state => {
const authenticated = isAuthenticated(state); const authenticated = isAuthenticated(state);
const loading = isLoginPending(state); const loading = isLoginPending(state);
const error = getLoginFailure(state); const error = getLoginFailure(state);
const link = getLoginLink(state);
return { return {
authenticated, authenticated,
loading, loading,
error error,
link
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
login: (username: string, password: string) => login: (loginLink: string, username: string, password: string) =>
dispatch(login(username, password)) dispatch(login(loginLink, username, password))
}; };
}; };

View File

@@ -11,14 +11,16 @@ import {
getLogoutFailure getLogoutFailure
} from "../modules/auth"; } from "../modules/auth";
import { Loading, ErrorPage } from "@scm-manager/ui-components"; import { Loading, ErrorPage } from "@scm-manager/ui-components";
import { fetchIndexResources, getLogoutLink } from "../modules/indexResource";
type Props = { type Props = {
authenticated: boolean, authenticated: boolean,
loading: boolean, loading: boolean,
error: Error, error: Error,
logoutLink: string,
// dispatcher functions // dispatcher functions
logout: () => void, logout: (link: string) => void,
// context props // context props
t: string => string t: string => string
@@ -26,7 +28,7 @@ type Props = {
class Logout extends React.Component<Props> { class Logout extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.logout(); this.props.logout(this.props.logoutLink);
} }
render() { render() {
@@ -51,16 +53,18 @@ const mapStateToProps = state => {
const authenticated = isAuthenticated(state); const authenticated = isAuthenticated(state);
const loading = isLogoutPending(state); const loading = isLogoutPending(state);
const error = getLogoutFailure(state); const error = getLogoutFailure(state);
const logoutLink = getLogoutLink(state);
return { return {
authenticated, authenticated,
loading, loading,
error error,
logoutLink
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
logout: () => dispatch(logout()) logout: (link: string) => dispatch(logout(link))
}; };
}; };

View File

@@ -1,14 +1,13 @@
//@flow //@flow
import React from "react"; 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 Overview from "../repos/containers/Overview";
import Users from "../users/containers/Users"; import Users from "../users/containers/Users";
import Login from "../containers/Login"; import Login from "../containers/Login";
import Logout from "../containers/Logout"; import Logout from "../containers/Logout";
import { Switch } from "react-router-dom";
import { ProtectedRoute } from "@scm-manager/ui-components"; import { ProtectedRoute } from "@scm-manager/ui-components";
import AddUser from "../users/containers/AddUser"; import AddUser from "../users/containers/AddUser";
import SingleUser from "../users/containers/SingleUser"; import SingleUser from "../users/containers/SingleUser";

View File

@@ -1,9 +1,12 @@
// @flow // @flow
import * as React from "react"; import * as React from "react";
import { apiClient, Loading } from "@scm-manager/ui-components"; import { apiClient, Loading } from "@scm-manager/ui-components";
import { getUiPluginsLink } from "../modules/indexResource";
import { connect } from "react-redux";
type Props = { type Props = {
children: React.Node children: React.Node,
link: string
}; };
type State = { type State = {
@@ -29,8 +32,13 @@ class PluginLoader extends React.Component<Props, State> {
this.setState({ this.setState({
message: "loading plugin information" message: "loading plugin information"
}); });
apiClient
.get("ui/plugins") this.getPlugins(this.props.link);
}
getPlugins = (link: string): Promise<any> => {
return apiClient
.get(link)
.then(response => response.text()) .then(response => response.text())
.then(JSON.parse) .then(JSON.parse)
.then(pluginCollection => pluginCollection._embedded.plugins) .then(pluginCollection => pluginCollection._embedded.plugins)
@@ -40,7 +48,7 @@ class PluginLoader extends React.Component<Props, State> {
finished: true finished: true
}); });
}); });
} };
loadPlugins = (plugins: Plugin[]) => { loadPlugins = (plugins: Plugin[]) => {
this.setState({ this.setState({
@@ -87,4 +95,11 @@ class PluginLoader extends React.Component<Props, State> {
} }
} }
export default PluginLoader; const mapStateToProps = state => {
const link = getUiPluginsLink(state);
return {
link
};
};
export default connect(mapStateToProps)(PluginLoader);

View File

@@ -7,14 +7,18 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
import users from "./users/modules/users"; import users from "./users/modules/users";
import repos from "./repos/modules/repos"; import repos from "./repos/modules/repos";
import repositoryTypes from "./repos/modules/repositoryTypes"; import repositoryTypes from "./repos/modules/repositoryTypes";
import changesets from "./repos/modules/changesets";
import sources from "./repos/sources/modules/sources";
import groups from "./groups/modules/groups"; import groups from "./groups/modules/groups";
import auth from "./modules/auth"; import auth from "./modules/auth";
import pending from "./modules/pending"; import pending from "./modules/pending";
import failure from "./modules/failure"; import failure from "./modules/failure";
import permissions from "./repos/permissions/modules/permissions"; import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config"; import config from "./config/modules/config";
import indexResources from "./modules/indexResource";
import type { BrowserHistory } from "history/createBrowserHistory"; import type { BrowserHistory } from "history/createBrowserHistory";
import branches from "./repos/modules/branches";
function createReduxStore(history: BrowserHistory) { function createReduxStore(history: BrowserHistory) {
const composeEnhancers = const composeEnhancers =
@@ -24,13 +28,17 @@ function createReduxStore(history: BrowserHistory) {
router: routerReducer, router: routerReducer,
pending, pending,
failure, failure,
indexResources,
users, users,
repos, repos,
repositoryTypes, repositoryTypes,
changesets,
branches,
permissions, permissions,
groups, groups,
auth, auth,
config config,
sources
}); });
return createStore( return createStore(

View File

@@ -9,18 +9,21 @@ import {
createGroup, createGroup,
isCreateGroupPending, isCreateGroupPending,
getCreateGroupFailure, getCreateGroupFailure,
createGroupReset createGroupReset,
getCreateGroupLink
} from "../modules/groups"; } from "../modules/groups";
import type { Group } from "@scm-manager/ui-types"; import type { Group } from "@scm-manager/ui-types";
import type { History } from "history"; import type { History } from "history";
import { getGroupsLink } from "../../modules/indexResource";
type Props = { type Props = {
t: string => string, t: string => string,
createGroup: (group: Group, callback?: () => void) => void, createGroup: (link: string, group: Group, callback?: () => void) => void,
history: History, history: History,
loading?: boolean, loading?: boolean,
error?: Error, error?: Error,
resetForm: () => void resetForm: () => void,
createLink: string
}; };
type State = {}; type State = {};
@@ -51,14 +54,14 @@ class AddGroup extends React.Component<Props, State> {
this.props.history.push("/groups"); this.props.history.push("/groups");
}; };
createGroup = (group: Group) => { createGroup = (group: Group) => {
this.props.createGroup(group, this.groupCreated); this.props.createGroup(this.props.createLink, group, this.groupCreated);
}; };
} }
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
createGroup: (group: Group, callback?: () => void) => createGroup: (link: string, group: Group, callback?: () => void) =>
dispatch(createGroup(group, callback)), dispatch(createGroup(link, group, callback)),
resetForm: () => { resetForm: () => {
dispatch(createGroupReset()); dispatch(createGroupReset());
} }
@@ -68,7 +71,9 @@ const mapDispatchToProps = dispatch => {
const mapStateToProps = state => { const mapStateToProps = state => {
const loading = isCreateGroupPending(state); const loading = isCreateGroupPending(state);
const error = getCreateGroupFailure(state); const error = getCreateGroupFailure(state);
const createLink = getGroupsLink(state);
return { return {
createLink,
loading, loading,
error error
}; };

View File

@@ -18,6 +18,7 @@ import {
isPermittedToCreateGroups, isPermittedToCreateGroups,
selectListAsCollection selectListAsCollection
} from "../modules/groups"; } from "../modules/groups";
import { getGroupsLink } from "../../modules/indexResource";
type Props = { type Props = {
groups: Group[], groups: Group[],
@@ -26,19 +27,20 @@ type Props = {
canAddGroups: boolean, canAddGroups: boolean,
list: PagedCollection, list: PagedCollection,
page: number, page: number,
groupLink: string,
// context objects // context objects
t: string => string, t: string => string,
history: History, history: History,
// dispatch functions // dispatch functions
fetchGroupsByPage: (page: number) => void, fetchGroupsByPage: (link: string, page: number) => void,
fetchGroupsByLink: (link: string) => void fetchGroupsByLink: (link: string) => void
}; };
class Groups extends React.Component<Props> { class Groups extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchGroupsByPage(this.props.page); this.props.fetchGroupsByPage(this.props.groupLink, this.props.page);
} }
onPageChange = (link: string) => { onPageChange = (link: string) => {
@@ -111,20 +113,23 @@ const mapStateToProps = (state, ownProps) => {
const canAddGroups = isPermittedToCreateGroups(state); const canAddGroups = isPermittedToCreateGroups(state);
const list = selectListAsCollection(state); const list = selectListAsCollection(state);
const groupLink = getGroupsLink(state);
return { return {
groups, groups,
loading, loading,
error, error,
canAddGroups, canAddGroups,
list, list,
page page,
groupLink
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchGroupsByPage: (page: number) => { fetchGroupsByPage: (link: string, page: number) => {
dispatch(fetchGroupsByPage(page)); dispatch(fetchGroupsByPage(link, page));
}, },
fetchGroupsByLink: (link: string) => { fetchGroupsByLink: (link: string) => {
dispatch(fetchGroupsByLink(link)); dispatch(fetchGroupsByLink(link));

View File

@@ -26,16 +26,18 @@ import {
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import EditGroup from "./EditGroup"; import EditGroup from "./EditGroup";
import { getGroupsLink } from "../../modules/indexResource";
type Props = { type Props = {
name: string, name: string,
group: Group, group: Group,
loading: boolean, loading: boolean,
error: Error, error: Error,
groupLink: string,
// dispatcher functions // dispatcher functions
deleteGroup: (group: Group, callback?: () => void) => void, deleteGroup: (group: Group, callback?: () => void) => void,
fetchGroup: string => void, fetchGroup: (string, string) => void,
// context objects // context objects
t: string => string, t: string => string,
@@ -45,7 +47,7 @@ type Props = {
class SingleGroup extends React.Component<Props> { class SingleGroup extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchGroup(this.props.name); this.props.fetchGroup(this.props.groupLink, this.props.name);
} }
stripEndingSlash = (url: string) => { stripEndingSlash = (url: string) => {
@@ -132,19 +134,21 @@ const mapStateToProps = (state, ownProps) => {
isFetchGroupPending(state, name) || isDeleteGroupPending(state, name); isFetchGroupPending(state, name) || isDeleteGroupPending(state, name);
const error = const error =
getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name); getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name);
const groupLink = getGroupsLink(state);
return { return {
name, name,
group, group,
loading, loading,
error error,
groupLink
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
fetchGroup: (name: string) => { fetchGroup: (link: string, name: string) => {
dispatch(fetchGroup(name)); dispatch(fetchGroup(link, name));
}, },
deleteGroup: (group: Group, callback?: () => void) => { deleteGroup: (group: Group, callback?: () => void) => {
dispatch(deleteGroup(group, callback)); dispatch(deleteGroup(group, callback));

View File

@@ -32,17 +32,16 @@ export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`;
export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`; export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`;
export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`; export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`;
const GROUPS_URL = "groups";
const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2"; const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2";
// fetch groups // fetch groups
export function fetchGroups() { export function fetchGroups(link: string) {
return fetchGroupsByLink(GROUPS_URL); return fetchGroupsByLink(link);
} }
export function fetchGroupsByPage(page: number) { export function fetchGroupsByPage(link: string, page: number) {
// backend start counting by 0 // backend start counting by 0
return fetchGroupsByLink(GROUPS_URL + "?page=" + (page - 1)); return fetchGroupsByLink(link + "?page=" + (page - 1));
} }
export function fetchGroupsByLink(link: string) { export function fetchGroupsByLink(link: string) {
@@ -56,7 +55,7 @@ export function fetchGroupsByLink(link: string) {
}) })
.catch(cause => { .catch(cause => {
const error = new Error(`could not fetch groups: ${cause.message}`); const error = new Error(`could not fetch groups: ${cause.message}`);
dispatch(fetchGroupsFailure(GROUPS_URL, error)); dispatch(fetchGroupsFailure(link, error));
}); });
}; };
} }
@@ -85,8 +84,8 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
} }
//fetch group //fetch group
export function fetchGroup(name: string) { export function fetchGroup(link: string, name: string) {
const groupUrl = GROUPS_URL + "/" + name; const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchGroupPending(name)); dispatch(fetchGroupPending(name));
return apiClient return apiClient
@@ -132,11 +131,11 @@ export function fetchGroupFailure(name: string, error: Error): Action {
} }
//create group //create group
export function createGroup(group: Group, callback?: () => void) { export function createGroup(link: string, group: Group, callback?: () => void) {
return function(dispatch: Dispatch) { return function(dispatch: Dispatch) {
dispatch(createGroupPending()); dispatch(createGroupPending());
return apiClient return apiClient
.post(GROUPS_URL, group, CONTENT_TYPE_GROUP) .post(link, group, CONTENT_TYPE_GROUP)
.then(() => { .then(() => {
dispatch(createGroupSuccess()); dispatch(createGroupSuccess());
if (callback) { if (callback) {
@@ -410,6 +409,12 @@ export const isPermittedToCreateGroups = (state: Object): boolean => {
return false; return false;
}; };
export function getCreateGroupLink(state: Object) {
if (state.groups.list.entry && state.groups.list.entry._links)
return state.groups.list.entry._links.create.href;
return undefined;
}
export function getGroupsFromState(state: Object) { export function getGroupsFromState(state: Object) {
const groupNames = selectList(state).entries; const groupNames = selectList(state).entries;
if (!groupNames) { if (!groupNames) {

View File

@@ -42,9 +42,11 @@ import reducer, {
modifyGroup, modifyGroup,
MODIFY_GROUP_PENDING, MODIFY_GROUP_PENDING,
MODIFY_GROUP_SUCCESS, MODIFY_GROUP_SUCCESS,
MODIFY_GROUP_FAILURE MODIFY_GROUP_FAILURE,
getCreateGroupLink
} from "./groups"; } from "./groups";
const GROUPS_URL = "/api/v2/groups"; const GROUPS_URL = "/api/v2/groups";
const URL = "/groups";
const error = new Error("You have an error!"); const error = new Error("You have an error!");
@@ -150,7 +152,7 @@ describe("groups fetch()", () => {
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroups()).then(() => { return store.dispatch(fetchGroups(URL)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
@@ -161,7 +163,7 @@ describe("groups fetch()", () => {
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroups()).then(() => { return store.dispatch(fetchGroups(URL)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUPS_PENDING); expect(actions[0].type).toEqual(FETCH_GROUPS_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUPS_FAILURE); expect(actions[1].type).toEqual(FETCH_GROUPS_FAILURE);
@@ -173,7 +175,7 @@ describe("groups fetch()", () => {
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup); fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroup("humanGroup")).then(() => { return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS); expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
@@ -187,7 +189,7 @@ describe("groups fetch()", () => {
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchGroup("humanGroup")).then(() => { return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING); expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE); expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
@@ -195,14 +197,13 @@ describe("groups fetch()", () => {
}); });
}); });
it("should successfully create group", () => { it("should successfully create group", () => {
fetchMock.postOnce(GROUPS_URL, { fetchMock.postOnce(GROUPS_URL, {
status: 201 status: 201
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(createGroup(humanGroup)).then(() => { return store.dispatch(createGroup(URL, humanGroup)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING); expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS); expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
@@ -219,7 +220,7 @@ describe("groups fetch()", () => {
called = true; called = true;
}; };
const store = mockStore({}); const store = mockStore({});
return store.dispatch(createGroup(humanGroup, callMe)).then(() => { return store.dispatch(createGroup(URL, humanGroup, callMe)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING); expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS); expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
@@ -227,14 +228,13 @@ describe("groups fetch()", () => {
}); });
}); });
it("should fail creating group on HTTP 500", () => { it("should fail creating group on HTTP 500", () => {
fetchMock.postOnce(GROUPS_URL, { fetchMock.postOnce(GROUPS_URL, {
status: 500 status: 500
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(createGroup(humanGroup)).then(() => { return store.dispatch(createGroup(URL, humanGroup)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING); expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE); expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE);
@@ -337,13 +337,10 @@ describe("groups fetch()", () => {
expect(actions[1].payload).toBeDefined(); expect(actions[1].payload).toBeDefined();
}); });
}); });
}); });
describe("groups reducer", () => { describe("groups reducer", () => {
it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => { it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => {
const newState = reducer({}, fetchGroupsSuccess(responseBody)); const newState = reducer({}, fetchGroupsSuccess(responseBody));
expect(newState.list).toEqual({ expect(newState.list).toEqual({
@@ -391,7 +388,6 @@ describe("groups reducer", () => {
expect(newState.byNames["humanGroup"]).toBeTruthy(); expect(newState.byNames["humanGroup"]).toBeTruthy();
}); });
it("should update state according to FETCH_GROUP_SUCCESS action", () => { it("should update state according to FETCH_GROUP_SUCCESS action", () => {
const newState = reducer({}, fetchGroupSuccess(emptyGroup)); const newState = reducer({}, fetchGroupSuccess(emptyGroup));
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup); expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
@@ -426,7 +422,6 @@ describe("groups reducer", () => {
expect(newState.byNames["emptyGroup"]).toBeFalsy(); expect(newState.byNames["emptyGroup"]).toBeFalsy();
expect(newState.list.entries).toEqual(["humanGroup"]); expect(newState.list.entries).toEqual(["humanGroup"]);
}); });
}); });
describe("selector tests", () => { describe("selector tests", () => {
@@ -476,6 +471,23 @@ describe("selector tests", () => {
expect(isPermittedToCreateGroups(state)).toBe(true); expect(isPermittedToCreateGroups(state)).toBe(true);
}); });
it("should return create Group link", () => {
const state = {
groups: {
list: {
entry: {
_links: {
create: {
href: "/create"
}
}
}
}
}
};
expect(getCreateGroupLink(state)).toBe("/create");
});
it("should get groups from state", () => { it("should get groups from state", () => {
const state = { const state = {
groups: { groups: {
@@ -560,9 +572,13 @@ describe("selector tests", () => {
}); });
it("should return true if create group is pending", () => { it("should return true if create group is pending", () => {
expect(isCreateGroupPending({pending: { expect(
isCreateGroupPending({
pending: {
[CREATE_GROUP]: true [CREATE_GROUP]: true
}})).toBeTruthy(); }
})
).toBeTruthy();
}); });
it("should return false if create group is not pending", () => { it("should return false if create group is not pending", () => {
@@ -570,18 +586,19 @@ describe("selector tests", () => {
}); });
it("should return error if creating group failed", () => { it("should return error if creating group failed", () => {
expect(getCreateGroupFailure({ expect(
getCreateGroupFailure({
failure: { failure: {
[CREATE_GROUP]: error [CREATE_GROUP]: error
} }
})).toEqual(error); })
).toEqual(error);
}); });
it("should return undefined if creating group did not fail", () => { it("should return undefined if creating group did not fail", () => {
expect(getCreateGroupFailure({})).toBeUndefined(); expect(getCreateGroupFailure({})).toBeUndefined();
}); });
it("should return true, when delete group humanGroup is pending", () => { it("should return true, when delete group humanGroup is pending", () => {
const state = { const state = {
pending: { pending: {

View File

@@ -1,7 +1,7 @@
// @flow // @flow
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import App from "./containers/App"; import Index from "./containers/Index";
import registerServiceWorker from "./registerServiceWorker"; import registerServiceWorker from "./registerServiceWorker";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
@@ -37,9 +37,7 @@ ReactDOM.render(
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
{/* ConnectedRouter will use the store from Provider automatically */} {/* ConnectedRouter will use the store from Provider automatically */}
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<PluginLoader> <Index />
<App />
</PluginLoader>
</ConnectedRouter> </ConnectedRouter>
</I18nextProvider> </I18nextProvider>
</Provider>, </Provider>,

View File

@@ -5,6 +5,12 @@ import * as types from "./types";
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components"; import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
import { isPending } from "./pending"; import { isPending } from "./pending";
import { getFailure } from "./failure"; import { getFailure } from "./failure";
import {
callFetchIndexResources,
FETCH_INDEXRESOURCES_SUCCESS,
fetchIndexResources, fetchIndexResourcesPending,
fetchIndexResourcesSuccess
} from "./indexResource";
// Action // Action
@@ -121,16 +127,11 @@ export const fetchMeFailure = (error: Error) => {
}; };
}; };
// urls
const ME_URL = "/me";
const LOGIN_URL = "/auth/access_token";
// side effects // side effects
const callFetchMe = (): Promise<Me> => { const callFetchMe = (link: string): Promise<Me> => {
return apiClient return apiClient
.get(ME_URL) .get(link)
.then(response => { .then(response => {
return response.json(); return response.json();
}) })
@@ -139,7 +140,11 @@ const callFetchMe = (): Promise<Me> => {
}); });
}; };
export const login = (username: string, password: string) => { export const login = (
loginLink: string,
username: string,
password: string
) => {
const login_data = { const login_data = {
cookie: true, cookie: true,
grant_type: "password", grant_type: "password",
@@ -149,9 +154,15 @@ export const login = (username: string, password: string) => {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(loginPending()); dispatch(loginPending());
return apiClient return apiClient
.post(LOGIN_URL, login_data) .post(loginLink, login_data)
.then(response => { .then(response => {
return callFetchMe(); dispatch(fetchIndexResourcesPending())
return callFetchIndexResources();
})
.then(response => {
dispatch(fetchIndexResourcesSuccess(response));
const meLink = response._links.me.href;
return callFetchMe(meLink);
}) })
.then(me => { .then(me => {
dispatch(loginSuccess(me)); dispatch(loginSuccess(me));
@@ -162,10 +173,10 @@ export const login = (username: string, password: string) => {
}; };
}; };
export const fetchMe = () => { export const fetchMe = (link: string) => {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchMePending()); dispatch(fetchMePending());
return callFetchMe() return callFetchMe(link)
.then(me => { .then(me => {
dispatch(fetchMeSuccess(me)); dispatch(fetchMeSuccess(me));
}) })
@@ -179,14 +190,17 @@ export const fetchMe = () => {
}; };
}; };
export const logout = () => { export const logout = (link: string) => {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(logoutPending()); dispatch(logoutPending());
return apiClient return apiClient
.delete(LOGIN_URL) .delete(link)
.then(() => { .then(() => {
dispatch(logoutSuccess()); dispatch(logoutSuccess());
}) })
.then(() => {
dispatch(fetchIndexResources());
})
.catch(error => { .catch(error => {
dispatch(logoutFailure(error)); dispatch(logoutFailure(error));
}); });

View File

@@ -32,6 +32,10 @@ import reducer, {
import configureMockStore from "redux-mock-store"; import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import {
FETCH_INDEXRESOURCES_PENDING,
FETCH_INDEXRESOURCES_SUCCESS
} from "./indexResource";
const me = { name: "tricia", displayName: "Tricia McMillian" }; const me = { name: "tricia", displayName: "Tricia McMillian" };
@@ -93,14 +97,28 @@ describe("auth actions", () => {
headers: { "content-type": "application/json" } headers: { "content-type": "application/json" }
}); });
const meLink = {
me: {
href: "/me"
}
};
fetchMock.getOnce("/api/v2/", {
_links: meLink
});
const expectedActions = [ const expectedActions = [
{ type: LOGIN_PENDING }, { type: LOGIN_PENDING },
{ type: FETCH_INDEXRESOURCES_PENDING },
{ type: FETCH_INDEXRESOURCES_SUCCESS, payload: { _links: meLink } },
{ type: LOGIN_SUCCESS, payload: me } { type: LOGIN_SUCCESS, payload: me }
]; ];
const store = mockStore({}); const store = mockStore({});
return store.dispatch(login("tricia", "secret123")).then(() => { return store
.dispatch(login("/auth/access_token", "tricia", "secret123"))
.then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
@@ -111,7 +129,9 @@ describe("auth actions", () => {
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(login("tricia", "secret123")).then(() => { return store
.dispatch(login("/auth/access_token", "tricia", "secret123"))
.then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(LOGIN_PENDING); expect(actions[0].type).toEqual(LOGIN_PENDING);
expect(actions[1].type).toEqual(LOGIN_FAILURE); expect(actions[1].type).toEqual(LOGIN_FAILURE);
@@ -135,7 +155,7 @@ describe("auth actions", () => {
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchMe()).then(() => { return store.dispatch(fetchMe("me")).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
@@ -146,7 +166,7 @@ describe("auth actions", () => {
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchMe()).then(() => { return store.dispatch(fetchMe("me")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_ME_PENDING); expect(actions[0].type).toEqual(FETCH_ME_PENDING);
expect(actions[1].type).toEqual(FETCH_ME_FAILURE); expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
@@ -166,7 +186,7 @@ describe("auth actions", () => {
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchMe()).then(() => { return store.dispatch(fetchMe("me")).then(() => {
// return of async actions // return of async actions
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
@@ -181,14 +201,23 @@ describe("auth actions", () => {
status: 401 status: 401
}); });
fetchMock.getOnce("/api/v2/", {
_links: {
login: {
login: "/login"
}
}
});
const expectedActions = [ const expectedActions = [
{ type: LOGOUT_PENDING }, { type: LOGOUT_PENDING },
{ type: LOGOUT_SUCCESS } { type: LOGOUT_SUCCESS },
{ type: FETCH_INDEXRESOURCES_PENDING }
]; ];
const store = mockStore({}); const store = mockStore({});
return store.dispatch(logout()).then(() => { return store.dispatch(logout("/auth/access_token")).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
@@ -199,7 +228,7 @@ describe("auth actions", () => {
}); });
const store = mockStore({}); const store = mockStore({});
return store.dispatch(logout()).then(() => { return store.dispatch(logout("/auth/access_token")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(LOGOUT_PENDING); expect(actions[0].type).toEqual(LOGOUT_PENDING);
expect(actions[1].type).toEqual(LOGOUT_FAILURE); expect(actions[1].type).toEqual(LOGOUT_FAILURE);

View 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");
}

View 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);
});
});

View 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;

View File

@@ -4,7 +4,6 @@ import "../../tests/enzyme";
import "../../tests/i18n"; import "../../tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context"; import ReactRouterEnzymeContext from "react-router-enzyme-context";
import PermissionsNavLink from "./PermissionsNavLink"; import PermissionsNavLink from "./PermissionsNavLink";
import EditNavLink from "./EditNavLink";
describe("PermissionsNavLink", () => { describe("PermissionsNavLink", () => {
const options = new ReactRouterEnzymeContext(); const options = new ReactRouterEnzymeContext();

View 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;

View 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");
});
});

View 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;

View 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