diff --git a/Jenkinsfile b/Jenkinsfile index aa6cd6b33b..6694b2eb7c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -34,6 +34,10 @@ node() { // No specific label mvn 'test -Dsonia.scm.test.skip.hg=true -Dmaven.test.failure.ignore=true' } + stage('Integration Test') { + mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true' + } + stage('SonarQube') { analyzeWith(mvn) diff --git a/pom.xml b/pom.xml index 7cbbf5df36..319bb62784 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,7 @@ scm-ui scm-webapp scm-server + scm-it diff --git a/scm-core/src/main/java/sonia/scm/ScmState.java b/scm-core/src/main/java/sonia/scm/ScmState.java index 5610a40c08..cd36aa707c 100644 --- a/scm-core/src/main/java/sonia/scm/ScmState.java +++ b/scm-core/src/main/java/sonia/scm/ScmState.java @@ -35,6 +35,7 @@ package sonia.scm; //~--- non-JDK imports -------------------------------------------------------- +import sonia.scm.repository.RepositoryType; import sonia.scm.security.PermissionDescriptor; import sonia.scm.user.User; @@ -82,9 +83,9 @@ public final class ScmState * @since 2.0.0 */ public ScmState(String version, User user, Collection groups, - String token, Collection repositoryTypes, String defaultUserType, - ScmClientConfig clientConfig, List assignedPermission, - List availablePermissions) + String token, Collection repositoryTypes, String defaultUserType, + ScmClientConfig clientConfig, List assignedPermission, + List availablePermissions) { this.version = version; this.user = user; @@ -165,7 +166,7 @@ public final class ScmState * * @return all available repository types */ - public Collection getRepositoryTypes() + public Collection getRepositoryTypes() { return repositoryTypes; } @@ -244,7 +245,7 @@ public final class ScmState /** Field description */ @XmlElement(name = "repositoryTypes") - private Collection repositoryTypes; + private Collection repositoryTypes; /** Field description */ private User user; diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java index 1aa6f474f0..7b71078f67 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java @@ -5,7 +5,7 @@ import com.google.common.base.Strings; import java.util.Objects; -public class NamespaceAndName { +public class NamespaceAndName implements Comparable { private final String namespace; private final String name; @@ -47,4 +47,13 @@ public class NamespaceAndName { public int hashCode() { return Objects.hash(namespace, name); } + + @Override + public int compareTo(NamespaceAndName o) { + int result = namespace.compareTo(o.namespace); + if (result == 0) { + return name.compareTo(o.name); + } + return result; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java index f972956adf..d3529294ed 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java @@ -2,7 +2,18 @@ package sonia.scm.repository; import sonia.scm.plugin.ExtensionPoint; +/** + * Strategy to create a namespace for the new repository. Namespaces are used to order and identify repositories. + */ @ExtensionPoint public interface NamespaceStrategy { - String getNamespace(); + + /** + * Create new namespace for the given repository. + * + * @param repository repository + * + * @return namespace + */ + String createNamespace(Repository repository); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java index 8dc5f8418b..739d0d0177 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java @@ -82,4 +82,7 @@ public interface RepositoryHandler * @since 1.15 */ public String getVersionInformation(); + + @Override + RepositoryType getType(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java index 775e4714d8..3742512622 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryIsNotArchivedException.java @@ -38,51 +38,11 @@ package sonia.scm.repository; * * @since 1.14 */ -public class RepositoryIsNotArchivedException extends RepositoryException -{ +public class RepositoryIsNotArchivedException extends RepositoryException { - /** Field description */ private static final long serialVersionUID = 7728748133123987511L; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - public RepositoryIsNotArchivedException() {} - - /** - * Constructs ... - * - * - * @param message - */ - public RepositoryIsNotArchivedException(String message) - { - super(message); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public RepositoryIsNotArchivedException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public RepositoryIsNotArchivedException(String message, Throwable cause) - { - super(message, cause); + public RepositoryIsNotArchivedException() { + super("Repository could not be deleted, because it is not archived."); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 493d8f6dbb..4fc9db5c32 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -35,13 +35,11 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Type; import sonia.scm.TypeManager; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Collection; -import java.util.Optional; //~--- JDK imports ------------------------------------------------------------ @@ -100,7 +98,7 @@ public interface RepositoryManager * * @return all configured repository types */ - public Collection getConfiguredTypes(); + public Collection getConfiguredTypes(); /** * Returns the {@link Repository} associated to the request uri. @@ -135,11 +133,4 @@ public interface RepositoryManager */ @Override public RepositoryHandler getHandler(String type); - - default Optional getByNamespace(String namespace, String name) { - return getAll() - .stream() - .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) - .findFirst(); - } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index 6990baf7c5..b504359bc0 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java @@ -103,7 +103,7 @@ public class RepositoryManagerDecorator * @return */ @Override - public Collection getConfiguredTypes() + public Collection getConfiguredTypes() { return decorated.getConfiguredTypes(); } diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index afac955f21..0e4abec020 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -6,6 +6,7 @@ import javax.ws.rs.core.MediaType; * Vendor media types used by SCMM. */ public class VndMediaType { + private static final String VERSION = "2"; private static final String TYPE = "application"; private static final String SUBTYPE_PREFIX = "vnd.scmm-"; @@ -20,8 +21,10 @@ public class VndMediaType { public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; - public static final String CONFIG = PREFIX + "config" + SUFFIX; + public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX; + public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; + public static final String ME = PREFIX + "me" + SUFFIX; private VndMediaType() { } diff --git a/scm-it/pom.xml b/scm-it/pom.xml new file mode 100644 index 0000000000..251fe4d957 --- /dev/null +++ b/scm-it/pom.xml @@ -0,0 +1,240 @@ + + + + 4.0.0 + + + sonia.scm + scm + 2.0.0-SNAPSHOT + + + sonia.scm + scm-it + jar + 2.0.0-SNAPSHOT + scm-it + + + + sonia.scm + scm-core + 2.0.0-SNAPSHOT + + + + sonia.scm + scm-test + 2.0.0-SNAPSHOT + + + + sonia.scm.plugins + scm-git-plugin + 2.0.0-SNAPSHOT + test + + + + sonia.scm.plugins + scm-git-plugin + 2.0.0-SNAPSHOT + tests + test + + + + sonia.scm.plugins + scm-hg-plugin + 2.0.0-SNAPSHOT + test + + + + sonia.scm.plugins + scm-hg-plugin + 2.0.0-SNAPSHOT + tests + test + + + + sonia.scm.plugins + scm-svn-plugin + 2.0.0-SNAPSHOT + test + + + + sonia.scm.plugins + scm-svn-plugin + 2.0.0-SNAPSHOT + tests + test + + + + io.rest-assured + rest-assured + 3.1.0 + test + + + + javax + javaee-api + 7.0 + test + + + + org.glassfish + javax.json + 1.0.4 + runtime + + + + + + + + com.mycila.maven-license-plugin + maven-license-plugin + 1.9.0 + +
http://download.scm-manager.org/licenses/mvn-license.txt
+ + src/** + **/test/** + + + target/** + .hg/** + + true +
+
+
+
+ + + + + it + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.12 + + + sonia/scm/it/*ITCase.java + + + + + integration-test + + integration-test + + + + verify + + verify + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.10 + + + + sonia.scm + scm-webapp + ${project.version} + war + ${project.build.outputDirectory} + scm-webapp.war + + + + + + copy-war + pre-integration-test + + copy + + + + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.maven.version} + + 8085 + STOP + + + scm.home + ${scm.home} + + + scm.stage + ${scm.stage} + + + java.awt.headless + true + + + + /scm + + ${project.basedir}/src/main/conf/jetty.xml + ${project.build.outputDirectory}/scm-webapp.war + 0 + true + + + + start-jetty + pre-integration-test + + deploy-war + + + + stop-jetty + post-integration-test + + stop + + + + + + + + + + DEVELOPMENT + target/scm-it + + + + + +
+ diff --git a/scm-it/src/main/conf/jetty.xml b/scm-it/src/main/conf/jetty.xml new file mode 100644 index 0000000000..ec7ac555c8 --- /dev/null +++ b/scm-it/src/main/conf/jetty.xml @@ -0,0 +1,72 @@ + + + + + + + + + 16384 + 16384 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java b/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java new file mode 100644 index 0000000000..e5dc7931d3 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java @@ -0,0 +1,29 @@ +package sonia.scm.it; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.util.regex.Pattern; + +class RegExMatcher extends BaseMatcher { + public static Matcher matchesPattern(String pattern) { + return new RegExMatcher(pattern); + } + + private final String pattern; + + private RegExMatcher(String pattern) { + this.pattern = pattern; + } + + @Override + public void describeTo(Description description) { + description.appendText("matching to regex pattern \"" + pattern + "\""); + } + + @Override + public boolean matches(Object o) { + return Pattern.compile(pattern).matcher(o.toString()).matches(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java new file mode 100644 index 0000000000..cd791cb013 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + */ + + +package sonia.scm.it; + +//~--- non-JDK imports -------------------------------------------------------- + +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import sonia.scm.repository.Person; +import sonia.scm.repository.client.api.ClientCommand; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientFactory; +import sonia.scm.web.VndMediaType; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.UUID; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static sonia.scm.it.RegExMatcher.matchesPattern; +import static sonia.scm.it.RestUtil.createResourceUrl; +import static sonia.scm.it.RestUtil.given; +import static sonia.scm.it.ScmTypes.availableScmTypes; +import static sonia.scm.it.TestData.repositoryJson; + +@RunWith(Parameterized.class) +public class RepositoriesITCase { + + public static final Person AUTHOR = new Person("SCM Administrator", "scmadmin@scm-manager.org"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final String repositoryType; + + private String repositoryUrl; + + public RepositoriesITCase(String repositoryType) { + this.repositoryType = repositoryType; + this.repositoryUrl = TestData.getDefaultRepositoryUrl(repositoryType); + } + + @Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + @Before + public void createRepository() { + TestData.createDefault(); + } + + @Test + public void shouldCreateSuccessfully() { + given(VndMediaType.REPOSITORY) + + .when() + .get(repositoryUrl) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "name", equalTo("HeartOfGold-" + repositoryType), + "type", equalTo(repositoryType), + "creationDate", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z"), + "lastModified", is(nullValue()), + "_links.self.href", equalTo(repositoryUrl) + ); + } + + @Test + public void shouldDeleteSuccessfully() { + given(VndMediaType.REPOSITORY) + + .when() + .delete(repositoryUrl) + + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + given(VndMediaType.REPOSITORY) + + .when() + .get(repositoryUrl) + + .then() + .statusCode(HttpStatus.SC_NOT_FOUND); + } + + @Test + public void shouldRejectMultipleCreations() { + String repositoryJson = repositoryJson(repositoryType); + given(VndMediaType.REPOSITORY) + .body(repositoryJson) + + .when() + .post(createResourceUrl("repositories")) + + .then() + .statusCode(HttpStatus.SC_CONFLICT); + } + + @Test + public void shouldCloneRepository() throws IOException { + RepositoryClient client = createRepositoryClient(); + assertEquals("expected metadata dir", 1, client.getWorkingCopy().list().length); + } + + @Test + public void shouldCommitFiles() throws IOException { + RepositoryClient client = createRepositoryClient(); + + for (int i = 0; i < 5; i++) { + createRandomFile(client); + } + + commit(client); + + RepositoryClient checkClient = createRepositoryClient(); + assertEquals("expected 5 files and metadata dir", 6, checkClient.getWorkingCopy().list().length); + } + + private static void createRandomFile(RepositoryClient client) throws IOException { + String uuid = UUID.randomUUID().toString(); + String name = "file-" + uuid + ".uuid"; + + File file = new File(client.getWorkingCopy(), name); + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(uuid.getBytes()); + } + + client.getAddCommand().add(name); + } + + private static void commit(RepositoryClient repositoryClient) throws IOException { + repositoryClient.getCommitCommand().commit(AUTHOR, "commit"); + if ( repositoryClient.isCommandSupported(ClientCommand.PUSH) ) { + repositoryClient.getPushCommand().push(); + } + } + + private RepositoryClient createRepositoryClient() throws IOException { + RepositoryClientFactory clientFactory = new RepositoryClientFactory(); + String cloneUrl = readCloneUrl(); + return clientFactory.create(repositoryType, cloneUrl, "scmadmin", "scmadmin", temporaryFolder.newFolder()); + } + + private String readCloneUrl() { + return given(VndMediaType.REPOSITORY) + + .when() + .get(repositoryUrl) + + .then() + .extract() + .path("_links.httpProtocol.href"); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/RestUtil.java new file mode 100644 index 0000000000..1458ab6d0b --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RestUtil.java @@ -0,0 +1,25 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import java.net.URI; + +import static java.net.URI.create; + +public class RestUtil { + + public static final URI BASE_URL = create("http://localhost:8081/scm/"); + public static final URI REST_BASE_URL = BASE_URL.resolve("api/rest/v2/"); + + public static URI createResourceUrl(String path) { + return REST_BASE_URL.resolve(path); + } + + public static RequestSpecification given(String mediaType) { + return RestAssured.given() + .contentType(mediaType) + .accept(mediaType) + .auth().preemptive().basic("scmadmin", "scmadmin"); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/ScmTypes.java b/scm-it/src/test/java/sonia/scm/it/ScmTypes.java new file mode 100644 index 0000000000..e8ba67e561 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/ScmTypes.java @@ -0,0 +1,21 @@ +package sonia.scm.it; + +import sonia.scm.util.IOUtil; + +import java.util.ArrayList; +import java.util.Collection; + +class ScmTypes { + static Collection availableScmTypes() { + Collection params = new ArrayList<>(); + + params.add("git"); + params.add("svn"); + + if (IOUtil.search("hg") != null) { + params.add("hg"); + } + + return params; + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java new file mode 100644 index 0000000000..b2785b2051 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -0,0 +1,116 @@ +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.web.VndMediaType; + +import javax.json.Json; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.asList; +import static sonia.scm.it.RestUtil.createResourceUrl; +import static sonia.scm.it.RestUtil.given; +import static sonia.scm.it.ScmTypes.availableScmTypes; + +public class TestData { + + private static final Logger LOG = LoggerFactory.getLogger(TestData.class); + + private static final List PROTECTED_USERS = asList("scmadmin", "anonymous"); + + private static Map DEFAULT_REPOSITORIES = new HashMap<>(); + + public static void createDefault() { + cleanup(); + createDefaultRepositories(); + } + + public static void cleanup() { + cleanupRepositories(); + cleanupGroups(); + cleanupUsers(); + } + + public static String getDefaultRepositoryUrl(String repositoryType) { + return DEFAULT_REPOSITORIES.get(repositoryType); + } + + private static void cleanupRepositories() { + List repositories = given(VndMediaType.REPOSITORY_COLLECTION) + .when() + .get(createResourceUrl("repositories")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.repositories._links.self.href"); + LOG.info("about to delete {} repositories", repositories.size()); + repositories.forEach(TestData::delete); + DEFAULT_REPOSITORIES.clear(); + } + + private static void cleanupGroups() { + List groups = given(VndMediaType.GROUP_COLLECTION) + .when() + .get(createResourceUrl("groups")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.groups._links.self.href"); + LOG.info("about to delete {} groups", groups.size()); + groups.forEach(TestData::delete); + } + + private static void cleanupUsers() { + List users = given(VndMediaType.USER_COLLECTION) + .when() + .get(createResourceUrl("users")) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.users._links.self.href"); + LOG.info("about to delete {} users", users.size()); + users.stream().filter(url -> PROTECTED_USERS.stream().noneMatch(url::contains)).forEach(TestData::delete); + } + + private static void delete(String url) { + given(VndMediaType.REPOSITORY) + .when() + .delete(url) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + LOG.info("deleted {}", url); + } + + private static void createDefaultRepositories() { + for (String repositoryType : availableScmTypes()) { + String url = given(VndMediaType.REPOSITORY) + .body(repositoryJson(repositoryType)) + + .when() + .post(createResourceUrl("repositories")) + + .then() + .statusCode(HttpStatus.SC_CREATED) + .extract() + .header("location"); + DEFAULT_REPOSITORIES.put(repositoryType, url); + } + } + + public static String repositoryJson(String repositoryType) { + return Json.createObjectBuilder() + .add("contact", "zaphod.beeblebrox@hitchhiker.com") + .add("description", "Heart of Gold") + .add("name", "HeartOfGold-" + repositoryType) + .add("archived", false) + .add("type", repositoryType) + .build().toString(); + } + + public static void main(String[] args) { + cleanup(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 2338cc3b46..87f96f850f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -41,7 +41,6 @@ import com.google.inject.Singleton; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import sonia.scm.Type; import sonia.scm.io.FileSystem; import sonia.scm.plugin.Extension; import sonia.scm.repository.spi.GitRepositoryServiceProvider; @@ -88,7 +87,7 @@ public class GitRepositoryHandler private static final Logger logger = LoggerFactory.getLogger(GitRepositoryHandler.class); /** Field description */ - public static final Type TYPE = new RepositoryType(TYPE_NAME, + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, GitRepositoryServiceProvider.COMMANDS); @@ -167,7 +166,7 @@ public class GitRepositoryHandler * @return */ @Override - public Type getType() + public RepositoryType getType() { return TYPE; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 40987fa4da..aad546f651 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -44,7 +44,6 @@ import org.slf4j.LoggerFactory; import sonia.scm.ConfigurationException; import sonia.scm.SCMContextProvider; -import sonia.scm.Type; import sonia.scm.installer.HgInstaller; import sonia.scm.installer.HgInstallerFactory; import sonia.scm.io.DirectoryFileFilter; @@ -98,7 +97,7 @@ public class HgRepositoryHandler public static final String TYPE_NAME = "hg"; /** Field description */ - public static final Type TYPE = new RepositoryType(TYPE_NAME, + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, HgRepositoryServiceProvider.COMMANDS, HgRepositoryServiceProvider.FEATURES); @@ -259,7 +258,7 @@ public class HgRepositoryHandler * @return */ @Override - public Type getType() + public RepositoryType getType() { return TYPE; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java index dcadd5c1c6..58ada0738b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java @@ -49,7 +49,6 @@ import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.util.SVNDebugLog; -import sonia.scm.Type; import sonia.scm.io.FileSystem; import sonia.scm.logging.SVNKitLogger; import sonia.scm.plugin.Extension; @@ -87,7 +86,7 @@ public class SvnRepositoryHandler public static final String TYPE_NAME = "svn"; /** Field description */ - public static final Type TYPE = new RepositoryType(TYPE_NAME, + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, SvnRepositoryServiceProvider.COMMANDS); @@ -150,7 +149,7 @@ public class SvnRepositoryHandler * @return */ @Override - public Type getType() + public RepositoryType getType() { return TYPE; } diff --git a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java index bf68c27b19..db4cfe0090 100644 --- a/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java +++ b/scm-test/src/main/java/sonia/scm/repository/DummyRepositoryHandler.java @@ -33,7 +33,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.Type; +import com.google.common.collect.Sets; import sonia.scm.io.DefaultFileSystem; import sonia.scm.store.ConfigurationStoreFactory; @@ -55,7 +55,7 @@ public class DummyRepositoryHandler public static final String TYPE_NAME = "dummy"; - public static final Type TYPE = new Type(TYPE_NAME, TYPE_DISPLAYNAME); + public static final RepositoryType TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, Sets.newHashSet()); private final Set existingRepoNames = new HashSet<>(); @@ -64,7 +64,7 @@ public class DummyRepositoryHandler } @Override - public Type getType() { + public RepositoryType getType() { return TYPE; } diff --git a/scm-ui/.vscode/settings.json b/scm-ui/.vscode/settings.json index f5d90fdf4b..d5728921ff 100644 --- a/scm-ui/.vscode/settings.json +++ b/scm-ui/.vscode/settings.json @@ -4,7 +4,7 @@ "editor.formatOnSave": false, // Enable per-language "[javascript]": { - "editor.formatOnSave": true + "editor.formatOnSave": false }, "flow.pathToFlow": "${workspaceRoot}/node_modules/.bin/flow" } diff --git a/scm-ui/package.json b/scm-ui/package.json index bd182cd972..6ce2e8880e 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -6,10 +6,12 @@ "dependencies": { "bulma": "^0.7.1", "classnames": "^2.2.5", + "font-awesome": "^4.7.0", "history": "^4.7.2", "i18next": "^11.4.0", "i18next-browser-languagedetector": "^2.2.2", "i18next-fetch-backend": "^0.1.0", + "moment": "^2.22.2", "react": "^16.4.1", "react-dom": "^16.4.1", "react-i18next": "^7.9.0", diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index e6d8fb305e..1feaa80a9b 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -31,7 +31,8 @@ "primary-navigation": { "repositories": "Repositories", "users": "Users", - "logout": "Logout" + "logout": "Logout", + "groups": "Groups" }, "paginator": { "next": "Next", diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json new file mode 100644 index 0000000000..95c42d4452 --- /dev/null +++ b/scm-ui/public/locales/en/groups.json @@ -0,0 +1,56 @@ +{ + "group": { + "name": "Name", + "description": "Description", + "creationDate": "Creation Date", + "lastModified": "Last Modified", + "type": "Type", + "members": "Members" + }, + "groups": { + "title": "Groups", + "subtitle": "Create, read, update and delete groups" + }, + "single-group": { + "error-title": "Error", + "error-subtitle": "Unknown group error", + "navigation-label": "Navigation", + "actions-label": "Actions", + "information-label": "Information", + "back-label": "Back" + }, + "add-group": { + "title": "Create Group", + "subtitle": "Create a new group" + }, + "create-group-button": { + "label": "Create" + }, + "edit-group-button": { + "label": "Edit" + }, + "add-member-button": { + "label": "Add member" + }, + "remove-member-button": { + "label": "Remove member" + }, + "add-member-textfield": { + "label": "Add member", + "error": "Invalid member name" + }, + "group-form": { + "submit": "Submit", + "name-error": "Group name is invalid", + "description-error": "Description is invalid" + }, + "delete-group-button": { + "label": "Delete", + "confirm-alert": { + "title": "Delete Group", + "message": "Do you really want to delete the group?", + "submit": "Yes", + "cancel": "No" + } + } +} diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json new file mode 100644 index 0000000000..7db2623247 --- /dev/null +++ b/scm-ui/public/locales/en/repos.json @@ -0,0 +1,46 @@ +{ + "repository": { + "name": "Name", + "type": "Type", + "contact": "Contact", + "description": "Description", + "creationDate": "Creation Date", + "lastModified": "Last Modified" + }, + "validation": { + "name-invalid": "The repository name is invalid", + "contact-invalid": "Contact must be a valid mail address" + }, + "overview": { + "title": "Repositories", + "subtitle": "Overview of available repositories", + "create-button": "Create" + }, + "repository-root": { + "error-title": "Error", + "error-subtitle": "Unknown repository error", + "actions-label": "Actions", + "back-label": "Back", + "navigation-label": "Navigation", + "information": "Information" + }, + "create": { + "title": "Create Repository", + "subtitle": "Create a new repository" + }, + "repository-form": { + "submit": "Save" + }, + "edit-nav-link": { + "label": "Edit" + }, + "delete-nav-action": { + "label": "Delete", + "confirm-alert": { + "title": "Delete repository", + "message": "Do you really want to delete the repository?", + "submit": "Yes", + "cancel": "No" + } + } +} diff --git a/scm-ui/public/locales/en/repositories.json b/scm-ui/public/locales/en/repositories.json deleted file mode 100644 index 4cfd96a25f..0000000000 --- a/scm-ui/public/locales/en/repositories.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "repositories": { - "title": "Repositories", - "subtitle": "Repositories will be shown here", - "body": "Coming soon ..." - } -} diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index c0b55adf7e..f1d71d8efc 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -5,7 +5,10 @@ "mail": "E-Mail", "password": "Password", "admin": "Admin", - "active": "Active" + "active": "Active", + "type": "Type", + "creationDate": "Creation Date", + "lastModified": "Last Modified" }, "users": { "title": "Users", diff --git a/scm-ui/src/apiclient.js b/scm-ui/src/apiclient.js index bcddf040aa..f8cfc4db70 100644 --- a/scm-ui/src/apiclient.js +++ b/scm-ui/src/apiclient.js @@ -27,7 +27,7 @@ function handleStatusCode(response: Response) { } export function createUrl(url: string) { - if (url.indexOf("://") > 0) { + if (url.includes("://")) { return url; } let urlWithStartingSlash = url; @@ -42,26 +42,12 @@ class ApiClient { return fetch(createUrl(url), fetchOptions).then(handleStatusCode); } - post(url: string, payload: any) { - return this.httpRequestWithJSONBody(url, payload, "POST"); + post(url: string, payload: any, contentType: string = "application/json") { + return this.httpRequestWithJSONBody("POST", url, contentType, payload); } - postWithContentType(url: string, payload: any, contentType: string) { - return this.httpRequestWithContentType( - url, - "POST", - JSON.stringify(payload), - contentType - ); - } - - putWithContentType(url: string, payload: any, contentType: string) { - return this.httpRequestWithContentType( - url, - "PUT", - JSON.stringify(payload), - contentType - ); + put(url: string, payload: any, contentType: string = "application/json") { + return this.httpRequestWithJSONBody("PUT", url, contentType, payload); } delete(url: string): Promise { @@ -73,37 +59,14 @@ class ApiClient { } httpRequestWithJSONBody( - url: string, - payload: any, - method: string - ): Promise { - // let options: RequestOptions = { - // method: method, - // body: JSON.stringify(payload) - // }; - // options = Object.assign(options, fetchOptions); - // // $FlowFixMe - // options.headers["Content-Type"] = "application/json"; - - // return fetch(createUrl(url), options).then(handleStatusCode); - - return this.httpRequestWithContentType( - url, - method, - JSON.stringify(payload), - "application/json" - ).then(handleStatusCode); - } - - httpRequestWithContentType( - url: string, method: string, - payload: any, - contentType: string + url: string, + contentType: string, + payload: any ): Promise { let options: RequestOptions = { method: method, - body: payload + body: JSON.stringify(payload) }; options = Object.assign(options, fetchOptions); // $FlowFixMe diff --git a/scm-ui/src/components/DateFromNow.js b/scm-ui/src/components/DateFromNow.js new file mode 100644 index 0000000000..b47de49a3d --- /dev/null +++ b/scm-ui/src/components/DateFromNow.js @@ -0,0 +1,32 @@ +//@flow +import React from "react"; +import moment from "moment"; +import { translate } from "react-i18next"; + +type Props = { + date?: string, + + // context props + i18n: any +}; + +class DateFromNow extends React.Component { + static format(locale: string, date?: string) { + let fromNow = ""; + if (date) { + fromNow = moment(date) + .locale(locale) + .fromNow(); + } + return fromNow; + } + + render() { + const { i18n, date } = this.props; + + const fromNow = DateFromNow.format(i18n.language, date); + return {fromNow}; + } +} + +export default translate()(DateFromNow); diff --git a/scm-ui/src/components/MailLink.js b/scm-ui/src/components/MailLink.js new file mode 100644 index 0000000000..7d009cde85 --- /dev/null +++ b/scm-ui/src/components/MailLink.js @@ -0,0 +1,18 @@ +// @flow +import React from "react"; + +type Props = { + address?: string +}; + +class MailLink extends React.Component { + render() { + const { address } = this.props; + if (!address) { + return null; + } + return {address}; + } +} + +export default MailLink; diff --git a/scm-ui/src/components/Paginator.js b/scm-ui/src/components/Paginator.js index 5b306b8240..8ec05adec9 100644 --- a/scm-ui/src/components/Paginator.js +++ b/scm-ui/src/components/Paginator.js @@ -93,8 +93,9 @@ class Paginator extends React.Component { if (page + 1 < pageTotal) { links.push(this.renderPageButton(page + 1, "next")); - links.push(this.seperator()); } + if(page+2 < pageTotal) //if there exists pages between next and last + links.push(this.seperator()); if (page < pageTotal) { links.push(this.renderLastButton()); } diff --git a/scm-ui/src/components/buttons/AddButton.js b/scm-ui/src/components/buttons/AddButton.js index 6668aa32d1..de72bdaab9 100644 --- a/scm-ui/src/components/buttons/AddButton.js +++ b/scm-ui/src/components/buttons/AddButton.js @@ -4,7 +4,7 @@ import Button, { type ButtonProps } from "./Button"; class AddButton extends React.Component { render() { - return