diff --git a/Jenkinsfile b/Jenkinsfile index 57cc3b901f..c0ca5f33d0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,7 +125,7 @@ boolean isMainBranch() { boolean waitForQualityGateWebhookToBeCalled() { boolean isQualityGateSucceeded = true - timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example + timeout(time: 5, unit: 'MINUTES') { // Needed when there is no webhook for example def qGate = waitForQualityGate() echo "SonarQube Quality Gate status: ${qGate.status}" if (qGate.status != 'OK') { diff --git a/pom.xml b/pom.xml index 944ebb6eb6..df49023265 100644 --- a/pom.xml +++ b/pom.xml @@ -188,15 +188,14 @@ test - - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-lib ${ssp.version} - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-processor ${ssp.version} true @@ -765,7 +764,7 @@ 9.2.10.v20150310 - 967c8fd521 + 1.1.0 1.4.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index a3fa037c7b..3c90fec779 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -94,6 +94,12 @@ javax.ws.rs-api + + org.jboss.resteasy + resteasy-jaxrs + test + + @@ -160,14 +166,13 @@ provided - - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-lib - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-processor true diff --git a/scm-core/src/main/java/sonia/scm/GenericDAO.java b/scm-core/src/main/java/sonia/scm/GenericDAO.java index b63a96f733..003c73806c 100644 --- a/scm-core/src/main/java/sonia/scm/GenericDAO.java +++ b/scm-core/src/main/java/sonia/scm/GenericDAO.java @@ -114,4 +114,5 @@ public interface GenericDAO * @return all items */ public Collection getAll(); + } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 2925b5b6b4..14c458d923 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -47,6 +47,9 @@ public interface Manager extends HandlerBase, LastModifiedAware { + int DEFAULT_LIMIT = 5; + + /** * Reloads a object from store and overwrites all changes. * diff --git a/scm-core/src/main/java/sonia/scm/ReducedModelObject.java b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java new file mode 100644 index 0000000000..b8db8c3ee0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java @@ -0,0 +1,15 @@ +package sonia.scm; + + +/** + * This is a reduced form of a model object. + * It can be used as search result to avoid returning the whole object properties. + * + * @author Mohamed Karray + */ +public interface ReducedModelObject { + + String getId(); + + String getDisplayName(); +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java index fa975520c1..496c87b440 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java @@ -4,11 +4,11 @@ import java.net.URI; public interface ScmPathInfo { - String REST_API_PATH = "/api/rest"; + String REST_API_PATH = "/api"; URI getApiRestUri(); default URI getRootUri() { - return getApiRestUri().resolve("../.."); + return getApiRestUri().resolve(".."); } } diff --git a/scm-core/src/main/java/sonia/scm/config/Configuration.java b/scm-core/src/main/java/sonia/scm/config/Configuration.java index e9bf3528d5..823c50b155 100644 --- a/scm-core/src/main/java/sonia/scm/config/Configuration.java +++ b/scm-core/src/main/java/sonia/scm/config/Configuration.java @@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions; @StaticPermissions( value = "configuration", permissions = {"read", "write"}, - globalPermissions = {} + globalPermissions = {"list"} ) public interface Configuration extends PermissionObject { } diff --git a/scm-core/src/main/java/sonia/scm/filter/GZipFilter.java b/scm-core/src/main/java/sonia/scm/filter/GZipFilter.java deleted file mode 100644 index 49fa8ebebf..0000000000 --- a/scm-core/src/main/java/sonia/scm/filter/GZipFilter.java +++ /dev/null @@ -1,125 +0,0 @@ -/** - * 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.filter; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.Priority; -import sonia.scm.util.WebUtil; -import sonia.scm.web.filter.HttpFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Filter for gzip encoding. - * - * @author Sebastian Sdorra - * @since 1.15 - */ -@Priority(Filters.PRIORITY_PRE_BASEURL) -@WebElement(value = Filters.PATTERN_RESOURCE_REGEX, regex = true) -public class GZipFilter extends HttpFilter -{ - - /** - * the logger for GZipFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(GZipFilter.class); - - //~--- get methods ---------------------------------------------------------- - - /** - * Return the configuration for the gzip filter. - * - * - * @return gzip filter configuration - */ - public GZipFilterConfig getConfig() - { - return config; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Encodes the response, if the request has support for gzip encoding. - * - * - * @param request http request - * @param response http response - * @param chain filter chain - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) - throws IOException, ServletException - { - if (WebUtil.isGzipSupported(request)) - { - if (logger.isTraceEnabled()) - { - logger.trace("compress output with gzip"); - } - - GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, - config); - - chain.doFilter(request, wrappedResponse); - wrappedResponse.finishResponse(); - } - else - { - chain.doFilter(request, response); - } - } - - //~--- fields --------------------------------------------------------------- - - /** gzip filter configuration */ - private GZipFilterConfig config = new GZipFilterConfig(); -} diff --git a/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java new file mode 100644 index 0000000000..1fa525d4fd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/filter/GZipResponseFilter.java @@ -0,0 +1,24 @@ +package sonia.scm.filter; + +import lombok.extern.slf4j.Slf4j; +import sonia.scm.util.WebUtil; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.util.zip.GZIPOutputStream; + +@Provider +@Slf4j +public class GZipResponseFilter implements ContainerResponseFilter { + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + if (WebUtil.isGzipSupported(requestContext::getHeaderString)) { + log.trace("compress output with gzip"); + GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream()); + responseContext.getHeaders().add("Content-Encoding", "gzip"); + responseContext.setEntityStream(wrappedResponse); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java index 98d9dcc7a3..c0b3c2ee8b 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -42,6 +42,7 @@ import com.google.common.base.Objects; import com.google.common.collect.Lists; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.ReducedModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -55,16 +56,16 @@ import java.util.List; /** * Organizes users into a group for easier permissions management. - * + * * TODO for 2.0: Use a set instead of a list for members * * @author Sebastian Sdorra */ -@StaticPermissions("group") +@StaticPermissions(value = "group", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) public class Group extends BasicPropertiesAware - implements ModelObject, PermissionObject + implements ModelObject, PermissionObject, ReducedModelObject { /** Field description */ @@ -309,6 +310,11 @@ public class Group extends BasicPropertiesAware return name; } + @Override + public String getDisplayName() { + return description; + } + /** * Returns a timestamp of the last modified date of this group. * diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManager.java b/scm-core/src/main/java/sonia/scm/group/GroupManager.java index 288196894d..08057ae3db 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManager.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManager.java @@ -61,4 +61,14 @@ public interface GroupManager * @return all groups assigned to the given member */ public Collection getGroupsForMember(String member); + + + /** + * Returns a {@link java.util.Collection} of filtered objects + * + * @param filter the searched string + * @return filtered object from the store + */ + Collection autocomplete(String filter); + } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java index e2367d863c..ef6de4164c 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java @@ -109,6 +109,11 @@ public class GroupManagerDecorator return decorated.getGroupsForMember(member); } + @Override + public Collection autocomplete(String filter) { + return decorated.autocomplete(filter); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java b/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java index 59a705e36a..ca1018b7aa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java +++ b/scm-core/src/main/java/sonia/scm/repository/ChangesetPagingResult.java @@ -82,6 +82,22 @@ public class ChangesetPagingResult implements Iterable, Serializable { this.total = total; this.changesets = changesets; + this.branchName = null; + } + + /** + * Constructs a new changeset paging result for a specific branch. + * + * + * @param total total number of changesets + * @param changesets current list of fetched changesets + * @param branchName branch name this result was created for + */ + public ChangesetPagingResult(int total, List changesets, String branchName) + { + this.total = total; + this.changesets = changesets; + this.branchName = branchName; } //~--- methods -------------------------------------------------------------- @@ -158,6 +174,7 @@ public class ChangesetPagingResult implements Iterable, Serializable return MoreObjects.toStringHelper(this) .add("changesets", changesets) .add("total", total) + .add("branch", branchName) .toString(); //J+ } @@ -186,37 +203,35 @@ public class ChangesetPagingResult implements Iterable, Serializable return total; } - //~--- set methods ---------------------------------------------------------- - - /** - * Sets the current list of changesets. - * - * - * @param changesets current list of changesets - */ - public void setChangesets(List changesets) + void setChangesets(List changesets) { this.changesets = changesets; } - /** - * Sets the total number of changesets - * - * - * @param total total number of changesets - */ - public void setTotal(int total) + void setTotal(int total) { this.total = total; } + void setBranchName(String branchName) { + this.branchName = branchName; + } + + /** + * Returns the branch name this result was created for. This can either be an explicit branch ("give me all + * changesets for branch xyz") or an implicit one ("give me the changesets for the default"). + */ + public String getBranchName() { + return branchName; + } + //~--- fields --------------------------------------------------------------- - /** current list of changesets */ @XmlElement(name = "changeset") @XmlElementWrapper(name = "changesets") private List changesets; - /** total number of changesets */ private int total; + + private String branchName; } diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index cad36f2d88..19c5b31349 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -64,7 +64,7 @@ import java.util.List; ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") -public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject { +public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ private static final long serialVersionUID = 3486560714961909711L; diff --git a/scm-core/src/main/java/sonia/scm/search/SearchRequest.java b/scm-core/src/main/java/sonia/scm/search/SearchRequest.java index e3998e2c6a..63f34346c2 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchRequest.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchRequest.java @@ -70,6 +70,12 @@ public class SearchRequest this.ignoreCase = ignoreCase; } + public SearchRequest(String query, boolean ignoreCase, int maxResults) { + this.query = query; + this.ignoreCase = ignoreCase; + this.maxResults = maxResults; + } + //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index 97c6bb16c7..e9dc277e5f 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -41,6 +41,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.ReducedModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -55,11 +56,10 @@ import java.security.Principal; * * @author Sebastian Sdorra */ -@StaticPermissions("user") +@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "users") @XmlAccessorType(XmlAccessType.FIELD) -public class -User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject +public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject { /** Field description */ diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java index 1f5aee1f19..7829cd9974 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java @@ -39,6 +39,7 @@ import sonia.scm.Manager; import sonia.scm.search.Searchable; import java.text.MessageFormat; +import java.util.Collection; import java.util.function.Consumer; import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE; @@ -90,5 +91,13 @@ public interface UserManager return getDefaultType().equals(user.getType()); } + /** + * Returns a {@link java.util.Collection} of filtered objects + * + * @param filter the searched string + * @return filtered object from the store + */ + Collection autocomplete(String filter); + } diff --git a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java index 225681f9e6..f291e325c1 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java @@ -121,6 +121,11 @@ public class UserManagerDecorator extends ManagerDecorator return decorated.getDefaultType(); } + @Override + public Collection autocomplete(String filter) { + return decorated.autocomplete(filter); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-core/src/main/java/sonia/scm/util/WebUtil.java b/scm-core/src/main/java/sonia/scm/util/WebUtil.java index 2fc0876668..a9337b0598 100644 --- a/scm-core/src/main/java/sonia/scm/util/WebUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/WebUtil.java @@ -49,6 +49,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; +import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -266,7 +267,12 @@ public final class WebUtil */ public static boolean isGzipSupported(HttpServletRequest request) { - String enc = request.getHeader(HEADER_ACCEPTENCODING); + return isGzipSupported(request::getHeader); + } + + public static boolean isGzipSupported(Function headerResolver) + { + String enc = headerResolver.apply(HEADER_ACCEPTENCODING); return (enc != null) && enc.contains("gzip"); } diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java new file mode 100644 index 0000000000..1baecb62af --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java @@ -0,0 +1,40 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Map; + +public abstract class JsonEnricherBase implements JsonEnricher { + + private final ObjectMapper objectMapper; + + protected JsonEnricherBase(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) { + return mediaType.equals(context.getResponseMediaType().toString()); + } + + protected JsonNode value(Object object) { + return objectMapper.convertValue(object, JsonNode.class); + } + + protected ObjectNode createObject() { + return objectMapper.createObjectNode(); + } + + protected ObjectNode createObject(Map values) { + ObjectNode object = createObject(); + + values.forEach((key, value) -> object.set(key, value(value))); + + return object; + } + + protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) { + ((ObjectNode) parent).set(newKey, child); + } +} 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 14350902a2..b6f2210d80 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -15,8 +15,10 @@ public class VndMediaType { public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX; public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION; + public static final String INDEX = PREFIX + "index" + SUFFIX; public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; + public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java index 8c910f92a9..676549a874 100644 --- a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java @@ -114,7 +114,7 @@ public class InitializingHttpScmProtocolWrapperTest { } private OngoingStubbing mockSetPathInfo() { - return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/")); + return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/")); } } diff --git a/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java new file mode 100644 index 0000000000..43ed4940fa --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java @@ -0,0 +1,51 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonEnricherBaseTest { + + private ObjectMapper objectMapper = new ObjectMapper(); + private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper); + + @Test + public void testResultHasMediaType() { + JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null); + + assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue(); + assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse(); + } + + @Test + public void testAppendLink() { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode links = objectMapper.createObjectNode(); + root.set("_links", links); + JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root); + enricher.enrich(context); + + assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link"); + } + + private static class TestJsonEnricher extends JsonEnricherBase { + + public TestJsonEnricher(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + public void enrich(JsonEnricherContext context) { + JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link"))); + + addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode); + } + } + +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 6b74fce7ca..b8d1e6f42e 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -35,17 +35,20 @@ package sonia.scm.xml; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.ImmutableList; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.GenericDAO; import sonia.scm.ModelObject; import sonia.scm.group.xml.XmlGroupDAO; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.store.ConfigurationStore; +import sonia.scm.util.AssertUtil; import java.util.Collection; -import sonia.scm.store.ConfigurationStore; +import java.util.stream.Collectors; + +//~--- JDK imports ------------------------------------------------------------ /** * diff --git a/scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java b/scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java new file mode 100644 index 0000000000..f343f322a3 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/AutoCompleteITCase.java @@ -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> 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))); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/IndexITCase.java b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java new file mode 100644 index 0000000000..4a621d962f --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java @@ -0,0 +1,48 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import org.apache.http.HttpStatus; +import org.junit.Test; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.web.VndMediaType; + +import static sonia.scm.it.utils.RegExMatcher.matchesPattern; +import static sonia.scm.it.utils.RestUtil.given; + +public class IndexITCase { + + @Test + public void shouldLinkEverythingForAdmin() { + given(VndMediaType.INDEX) + + .when() + .get(RestUtil.createResourceUrl("")) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "_links.repositories.href", matchesPattern(".+/repositories/"), + "_links.users.href", matchesPattern(".+/users/"), + "_links.groups.href", matchesPattern(".+/groups/"), + "_links.config.href", matchesPattern(".+/config"), + "_links.gitConfig.href", matchesPattern(".+/config/git"), + "_links.hgConfig.href", matchesPattern(".+/config/hg"), + "_links.svnConfig.href", matchesPattern(".+/config/svn") + ); + } + + @Test + public void shouldCreateLoginLinksForAnonymousAccess() { + RestAssured.given() // do not specify user credentials + + .when() + .get(RestUtil.createResourceUrl("")) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "_links.login.href", matchesPattern(".+/auth/.+") + ); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/MeITCase.java b/scm-it/src/test/java/sonia/scm/it/MeITCase.java index 1c7b5c9e03..5809f883ca 100644 --- a/scm-it/src/test/java/sonia/scm/it/MeITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/MeITCase.java @@ -20,12 +20,9 @@ public class MeITCase { String newPassword = TestData.USER_SCM_ADMIN + "1"; // admin change the own password ScmRequests.start() - .given() - .url(TestData.getMeUrl()) - .usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) - .getMeResource() + .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) + .requestMe() .assertStatusCode(200) - .usingMeResponse() .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertPassword(Assert::assertNull) .assertType(s -> assertThat(s).isEqualTo("xml")) @@ -33,12 +30,9 @@ public class MeITCase { .assertStatusCode(204); // assert password is changed -> login with the new Password than undo changes ScmRequests.start() - .given() - .url(TestData.getUserUrl(TestData.USER_SCM_ADMIN)) - .usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword) - .getMeResource() + .requestIndexResource(TestData.USER_SCM_ADMIN, newPassword) + .requestMe() .assertStatusCode(200) - .usingMeResponse() .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin .requestChangePassword(newPassword, TestData.USER_SCM_ADMIN) .assertStatusCode(204); @@ -49,31 +43,14 @@ public class MeITCase { String newUser = "user"; String password = "pass"; String type = "not XML Type"; - TestData.createUser(newUser, password, true, type); + TestData.createUser(newUser, password, true, type, "user@scm-manager.org"); ScmRequests.start() - .given() - .url(TestData.getMeUrl()) - .usernameAndPassword(newUser, password) - .getMeResource() + .requestIndexResource(newUser, password) + .requestMe() .assertStatusCode(200) - .usingMeResponse() .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertPassword(Assert::assertNull) .assertType(s -> assertThat(s).isEqualTo(type)) .assertPasswordLinkDoesNotExists(); } - - @Test - public void shouldGet403IfUserIsNotAdmin() { - String newUser = "user"; - String password = "pass"; - String type = "xml"; - TestData.createUser(newUser, password, false, type); - ScmRequests.start() - .given() - .url(TestData.getMeUrl()) - .usernameAndPassword(newUser, password) - .getMeResource() - .assertStatusCode(403); - } } diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index f288d4891c..8785f1d8ce 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -87,13 +87,13 @@ public class PermissionsITCase { @Before public void prepareEnvironment() { TestData.createDefault(); - TestData.createUser(USER_READ, USER_PASS); + TestData.createNotAdminUser(USER_READ, USER_PASS); TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType); - TestData.createUser(USER_WRITE, USER_PASS); + TestData.createNotAdminUser(USER_WRITE, USER_PASS); TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType); - TestData.createUser(USER_OWNER, USER_PASS); + TestData.createNotAdminUser(USER_OWNER, USER_PASS); TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); - TestData.createUser(USER_OTHER, USER_PASS); + TestData.createNotAdminUser(USER_OTHER, USER_PASS); createdPermissions = 3; } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index c49a65bea2..3c67ca3dc3 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -36,6 +36,7 @@ package sonia.scm.it; import org.apache.http.HttpStatus; import org.assertj.core.api.Assertions; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index 3f8832a3f5..334274b7b0 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -44,7 +44,7 @@ public class RepositoryAccessITCase { private final String repositoryType; private File folder; - private ScmRequests.AppliedRepositoryRequest repositoryGetRequest; + private ScmRequests.RepositoryResponse repositoryResponse; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -59,17 +59,13 @@ public class RepositoryAccessITCase { public void init() { TestData.createDefault(); folder = tempFolder.getRoot(); - repositoryGetRequest = ScmRequests.start() - .given() - .url(TestData.getDefaultRepositoryUrl(repositoryType)) - .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) - .getRepositoryResource() + String namespace = ADMIN_USERNAME; + String repo = TestData.getDefaultRepoName(repositoryType); + repositoryResponse = + ScmRequests.start() + .requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD) + .requestRepository(namespace, repo) .assertStatusCode(HttpStatus.SC_OK); - ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start() - .given() - .url(TestData.getMeUrl()) - .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) - .getMeResource(); } @Test @@ -306,17 +302,12 @@ public class RepositoryAccessITCase { public void shouldFindFileHistory() throws IOException { RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a"); - repositoryGetRequest - .usingRepositoryResponse() + repositoryResponse .requestSources() - .usingSourcesResponse() .requestSelf("folder") - .usingSourcesResponse() .requestSelf("subfolder") - .usingSourcesResponse() .requestFileHistory("a.txt") .assertStatusCode(HttpStatus.SC_OK) - .usingChangesetsResponse() .assertChangesets(changesets -> { assertThat(changesets).hasSize(1); assertThat(changesets.get(0)).containsEntry("id", changeset.getId()); @@ -332,14 +323,11 @@ public class RepositoryAccessITCase { String fileName = "a.txt"; Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a"); String revision = changeset.getId(); - repositoryGetRequest - .usingRepositoryResponse() + repositoryResponse .requestChangesets() .assertStatusCode(HttpStatus.SC_OK) - .usingChangesetsResponse() .requestModifications(revision) .assertStatusCode(HttpStatus.SC_OK) - .usingModificationsResponse() .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertAdded(addedFiles -> assertThat(addedFiles) .hasSize(1) @@ -359,14 +347,11 @@ public class RepositoryAccessITCase { Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName); String revision = changeset.getId(); - repositoryGetRequest - .usingRepositoryResponse() + repositoryResponse .requestChangesets() .assertStatusCode(HttpStatus.SC_OK) - .usingChangesetsResponse() .requestModifications(revision) .assertStatusCode(HttpStatus.SC_OK) - .usingModificationsResponse() .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertRemoved(removedFiles -> assertThat(removedFiles) .hasSize(1) @@ -386,14 +371,11 @@ public class RepositoryAccessITCase { Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content"); String revision = changeset.getId(); - repositoryGetRequest - .usingRepositoryResponse() + repositoryResponse .requestChangesets() .assertStatusCode(HttpStatus.SC_OK) - .usingChangesetsResponse() .requestModifications(revision) .assertStatusCode(HttpStatus.SC_OK) - .usingModificationsResponse() .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertModified(modifiedFiles -> assertThat(modifiedFiles) .hasSize(1) @@ -423,14 +405,11 @@ public class RepositoryAccessITCase { Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); String revision = changeset.getId(); - repositoryGetRequest - .usingRepositoryResponse() + repositoryResponse .requestChangesets() .assertStatusCode(HttpStatus.SC_OK) - .usingChangesetsResponse() .requestModifications(revision) .assertStatusCode(HttpStatus.SC_OK) - .usingModificationsResponse() .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertAdded(a -> assertThat(a) .hasSize(1) @@ -463,14 +442,11 @@ public class RepositoryAccessITCase { Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); String revision = changeset.getId(); - repositoryGetRequest - .usingRepositoryResponse() + repositoryResponse .requestChangesets() .assertStatusCode(HttpStatus.SC_OK) - .usingChangesetsResponse() .requestModifications(revision) .assertStatusCode(HttpStatus.SC_OK) - .usingModificationsResponse() .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) .assertAdded(a -> assertThat(a) .hasSize(3) diff --git a/scm-it/src/test/java/sonia/scm/it/UserITCase.java b/scm-it/src/test/java/sonia/scm/it/UserITCase.java index 67fe23dcbc..a4bc4e412f 100644 --- a/scm-it/src/test/java/sonia/scm/it/UserITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/UserITCase.java @@ -19,57 +19,48 @@ public class UserITCase { public void adminShouldChangeOwnPassword() { String newUser = "user"; String password = "pass"; - TestData.createUser(newUser, password, true, "xml"); + TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org"); String newPassword = "new_password"; // admin change the own password ScmRequests.start() - .given() - .url(TestData.getUserUrl(newUser)) - .usernameAndPassword(newUser, password) - .getUserResource() + .requestIndexResource(newUser, password) + .assertStatusCode(200) + .requestUser(newUser) .assertStatusCode(200) - .usingUserResponse() .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertPassword(Assert::assertNull) .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource .assertStatusCode(204); // assert password is changed -> login with the new Password ScmRequests.start() - .given() - .url(TestData.getUserUrl(newUser)) - .usernameAndPassword(newUser, newPassword) - .getUserResource() + .requestIndexResource(newUser, newPassword) .assertStatusCode(200) - .usingUserResponse() + .requestUser(newUser) .assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE)) .assertPassword(Assert::assertNull); - } @Test public void adminShouldChangePasswordOfOtherUser() { String newUser = "user"; String password = "pass"; - TestData.createUser(newUser, password, true, "xml"); + TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org"); String newPassword = "new_password"; // admin change the password of the user ScmRequests.start() - .given() - .url(TestData.getUserUrl(newUser))// the admin get the user object - .usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) - .getUserResource() + .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) + .assertStatusCode(200) + .requestUser(newUser) .assertStatusCode(200) - .usingUserResponse() .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin .assertPassword(Assert::assertNull) .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource .assertStatusCode(204); // assert password is changed ScmRequests.start() - .given() - .url(TestData.getUserUrl(newUser)) - .usernameAndPassword(newUser, newPassword) - .getUserResource() + .requestIndexResource(newUser, newPassword) + .assertStatusCode(200) + .requestUser(newUser) .assertStatusCode(200); } @@ -80,34 +71,15 @@ public class UserITCase { String newUser = "user"; String password = "pass"; String type = "not XML Type"; - TestData.createUser(newUser, password, true, type); + TestData.createUser(newUser, password, true, type, "user@scm-manager.org"); ScmRequests.start() - .given() - .url(TestData.getMeUrl()) - .usernameAndPassword(newUser, password) - .getUserResource() + .requestIndexResource(newUser, password) + .assertStatusCode(200) + .requestUser(newUser) .assertStatusCode(200) - .usingUserResponse() .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) .assertPassword(Assert::assertNull) .assertType(s -> assertThat(s).isEqualTo(type)) .assertPasswordLinkDoesNotExists(); } - - @Test - public void shouldGet403IfUserIsNotAdmin() { - String newUser = "user"; - String password = "pass"; - String type = "xml"; - TestData.createUser(newUser, password, false, type); - ScmRequests.start() - .given() - .url(TestData.getMeUrl()) - .usernameAndPassword(newUser, password) - .getUserResource() - .assertStatusCode(403); - } - - - } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java index 10386a682f..8fb9fdf798 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java @@ -24,6 +24,6 @@ public class RegExMatcher extends BaseMatcher { @Override public boolean matches(Object o) { - return Pattern.compile(pattern).matcher(o.toString()).matches(); + return o != null && Pattern.compile(pattern).matcher(o.toString()).matches(); } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java index c8b01a6d72..645cf06ac8 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java @@ -10,7 +10,7 @@ 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 final URI REST_BASE_URL = BASE_URL.resolve("api/v2/"); public static URI createResourceUrl(String path) { return REST_BASE_URL.resolve(path); diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java index 41fd9a1290..bcfbe93b3e 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -4,7 +4,6 @@ import io.restassured.RestAssured; import io.restassured.response.Response; import sonia.scm.web.VndMediaType; -import java.net.URI; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -33,8 +32,10 @@ public class ScmRequests { return new ScmRequests(); } - public Given given() { - return new Given(); + public IndexResponse requestIndexResource(String username, String password) { + setUsername(username); + setPassword(password); + return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString())); } @@ -46,24 +47,46 @@ public class ScmRequests { * @return the response of the GET request using the given link */ private Response applyGETRequestFromLink(Response response, String linkPropertyName) { - return applyGETRequest(response - .then() - .extract() - .path(linkPropertyName)); + return applyGETRequestFromLinkWithParams(response, linkPropertyName, ""); } + /** + * Apply a GET Request to the extracted url from the given link + * + * @param linkPropertyName the property name of link + * @param response the response containing the link + * @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name + * @return the response of the GET request using the given link + */ + private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) { + return applyGETRequestWithQueryParams(response + .then() + .extract() + .path(linkPropertyName), params); + } + + /** + * Apply a GET Request to the given url 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 url + */ + private Response applyGETRequestWithQueryParams(String url, String params) { + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .get(url + params); + } /** * Apply a GET Request to the given url and return the response. * * @param url the url of the GET request * @return the response of the GET request using the given url - */ + **/ private Response applyGETRequest(String url) { - return RestAssured.given() - .auth().preemptive().basic(username, password) - .when() - .get(url); + return applyGETRequestWithQueryParams(url, ""); } @@ -101,11 +124,6 @@ public class ScmRequests { .put(url); } - - private void setUrl(String url) { - this.url = url; - } - private void setUsername(String username) { this.username = username; } @@ -114,296 +132,185 @@ public class ScmRequests { this.password = password; } - private String getUrl() { - return url; - } - private String getUsername() { - return username; - } + public class IndexResponse extends ModelResponse { + 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"; - private String getPassword() { - return password; - } - - public class Given { - - public GivenUrl url(String url) { - setUrl(url); - return new GivenUrl(); + public IndexResponse(Response response) { + super(response, null); } - public GivenUrl url(URI url) { - setUrl(url.toString()); - return new GivenUrl(); + public AutoCompleteResponse requestAutoCompleteUsers(String q) { + return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_USERS, "?q=" + q), this); + } + + public AutoCompleteResponse requestAutoCompleteGroups(String q) { + return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_GROUPS, "?q=" + q), this); + } + + public RepositoryResponse requestRepository(String namespace, String name) { + return new RepositoryResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORIES, namespace + "/" + name), this); + } + + public MeResponse requestMe() { + return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this); + } + + public UserResponse requestUser(String username) { + return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this); } } - public class GivenWithUrlAndAuth { - public AppliedMeRequest getMeResource() { - return new AppliedMeRequest(applyGETRequest(url)); + public class RepositoryResponse extends ModelResponse, 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 AppliedUserRequest getUserResource() { - return new AppliedUserRequest(applyGETRequest(url)); + public SourcesResponse requestSources() { + return new SourcesResponse<>(applyGETRequestFromLink(response, LINKS_SOURCES), this); } - public AppliedRepositoryRequest getRepositoryResource() { - return new AppliedRepositoryRequest( - applyGETRequest(url) - ); - } - } - - public class 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 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 ChangesetsResponse requestChangesets() { + return new ChangesetsResponse<>(applyGETRequestFromLink(response, LINKS_CHANGESETS), this); } } - public class AppliedRepositoryRequest extends AppliedRequest { + public class ChangesetsResponse extends ModelResponse, PREV> { - 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 { - - public AppliedChangesetsRequest(Response response) { - super(response); - } - - public ChangesetsResponse usingChangesetsResponse() { - return new ChangesetsResponse(super.response); - } - } - - public class ChangesetsResponse { - private Response changesetsResponse; - - public ChangesetsResponse(Response changesetsResponse) { - this.changesetsResponse = changesetsResponse; + public ChangesetsResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } public ChangesetsResponse assertChangesets(Consumer> changesetsConsumer) { - List changesets = changesetsResponse.then().extract().path("_embedded.changesets"); + List changesets = response.then().extract().path("_embedded.changesets"); changesetsConsumer.accept(changesets); return this; } - public AppliedDiffRequest requestDiff(String revision) { - return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href")); + public DiffResponse requestDiff(String revision) { + return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this); } - public AppliedModificationsRequest requestModifications(String revision) { - return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href")); + public ModificationsResponse requestModifications(String revision) { + return new ModificationsResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"), this); } } - public class AppliedSourcesRequest extends AppliedRequest { - public AppliedSourcesRequest(Response sourcesResponse) { - super(sourcesResponse); - } + public class SourcesResponse extends ModelResponse, PREV> { - public SourcesResponse usingSourcesResponse() { - return new SourcesResponse(super.response); - } - } - - public class SourcesResponse { - - private Response sourcesResponse; - - public SourcesResponse(Response sourcesResponse) { - this.sourcesResponse = sourcesResponse; + public SourcesResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } public SourcesResponse assertRevision(Consumer assertRevision) { - String revision = sourcesResponse.then().extract().path("revision"); + String revision = response.then().extract().path("revision"); assertRevision.accept(revision); return this; } public SourcesResponse assertFiles(Consumer assertFiles) { - List files = sourcesResponse.then().extract().path("files"); + List files = response.then().extract().path("files"); assertFiles.accept(files); return this; } - public AppliedChangesetsRequest requestFileHistory(String fileName) { - return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href")); + public ChangesetsResponse requestFileHistory(String fileName) { + return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"), this); } - public AppliedSourcesRequest requestSelf(String fileName) { - return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href")); + public SourcesResponse requestSelf(String fileName) { + return new SourcesResponse<>(applyGETRequestFromLink(response, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"), this); } } - public class AppliedDiffRequest extends AppliedRequest { + public class ModificationsResponse extends ModelResponse, PREV> { - public AppliedDiffRequest(Response response) { - super(response); - } - } - - public class GivenUrl { - - public GivenWithUrlAndAuth usernameAndPassword(String username, String password) { - setUsername(username); - setPassword(password); - return new GivenWithUrlAndAuth(); - } - } - - public class AppliedModificationsRequest extends AppliedRequest { - public AppliedModificationsRequest(Response response) { - super(response); + public ModificationsResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } - 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 assertRevision) { - String revision = resource.then().extract().path("revision"); + public ModificationsResponse assertRevision(Consumer assertRevision) { + String revision = response.then().extract().path("revision"); assertRevision.accept(revision); return this; } - public ModificationsResponse assertAdded(Consumer> assertAdded) { - List added = resource.then().extract().path("added"); + public ModificationsResponse assertAdded(Consumer> assertAdded) { + List added = response.then().extract().path("added"); assertAdded.accept(added); return this; } - public ModificationsResponse assertRemoved(Consumer> assertRemoved) { - List removed = resource.then().extract().path("removed"); + public ModificationsResponse assertRemoved(Consumer> assertRemoved) { + List removed = response.then().extract().path("removed"); assertRemoved.accept(removed); return this; } - public ModificationsResponse assertModified(Consumer> assertModified) { - List modified = resource.then().extract().path("modified"); + public ModificationsResponse assertModified(Consumer> assertModified) { + List modified = response.then().extract().path("modified"); assertModified.accept(modified); return this; } } - public class AppliedMeRequest extends AppliedRequest { - - public AppliedMeRequest(Response response) { - super(response); - } - - public MeResponse usingMeResponse() { - return new MeResponse(super.response); - } - - } - - public class MeResponse extends UserResponse { + public class MeResponse extends UserResponse { - 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 MeResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } } - public class UserResponse extends ModelResponse { + public class UserResponse extends ModelResponse, PREV> { public static final String LINKS_PASSWORD_HREF = "_links.password.href"; - public UserResponse(Response response) { - super(response); + public UserResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } - public SELF assertPassword(Consumer assertPassword) { + public UserResponse assertPassword(Consumer assertPassword) { return super.assertSingleProperty(assertPassword, "password"); } - public SELF assertType(Consumer assertType) { + public UserResponse assertType(Consumer assertType) { return assertSingleProperty(assertType, "type"); } - public SELF assertAdmin(Consumer assertAdmin) { + public UserResponse assertAdmin(Consumer assertAdmin) { return assertSingleProperty(assertAdmin, "admin"); } - public SELF assertPasswordLinkDoesNotExists() { + public UserResponse assertPasswordLinkDoesNotExists() { return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF); } - public SELF assertPasswordLinkExists() { + public UserResponse assertPasswordLinkExists() { return assertPropertyPathExists(LINKS_PASSWORD_HREF); } - public AppliedChangePasswordRequest requestChangePassword(String newPassword) { - return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword))); + public ChangePasswordResponse requestChangePassword(String newPassword) { + return requestChangePassword(null, newPassword); + } + + public ChangePasswordResponse requestChangePassword(String oldPassword, String newPassword) { + return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this); } } @@ -412,12 +319,18 @@ public class ScmRequests { /** * encapsulate standard assertions over model properties */ - public class ModelResponse { + public class ModelResponse, PREV extends ModelResponse> { + protected PREV previousResponse; protected Response response; - public ModelResponse(Response response) { + public ModelResponse(Response response, PREV previousResponse) { this.response = response; + this.previousResponse = previousResponse; + } + + public PREV returnToPrevious() { + return previousResponse; } public SELF assertSingleProperty(Consumer assertSingleProperty, String propertyJsonPath) { @@ -441,25 +354,45 @@ public class ScmRequests { assertProperties.accept(properties); return (SELF) this; } + + /** + * special assertion of the status code + * + * @param expectedStatusCode the expected status code + * @return the self object + */ + public SELF assertStatusCode(int expectedStatusCode) { + this.response.then().assertThat().statusCode(expectedStatusCode); + return (SELF) this; + } } - public class AppliedChangePasswordRequest extends AppliedRequest { + public class AutoCompleteResponse extends ModelResponse, PREV> { - public AppliedChangePasswordRequest(Response response) { - super(response); + public AutoCompleteResponse(Response response, PREV previousResponse) { + super(response, previousResponse); + } + + public AutoCompleteResponse assertAutoCompleteResults(Consumer> checker) { + List result = response.then().extract().path(""); + checker.accept(result); + return this; } } - public class AppliedUserRequest extends AppliedRequest { - public AppliedUserRequest(Response response) { - super(response); + public class DiffResponse extends ModelResponse, PREV> { + + public DiffResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } + } - public UserResponse usingUserResponse() { - return new UserResponse(super.response); + public class ChangePasswordResponse extends ModelResponse, PREV> { + + public ChangePasswordResponse(Response response, PREV previousResponse) { + super(response, previousResponse); } - } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 03da80ea3b..a164fc6649 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -46,11 +46,11 @@ public class TestData { return DEFAULT_REPOSITORIES.get(repositoryType); } - public static void createUser(String username, String password) { - createUser(username, password, false, "xml"); + public static void createNotAdminUser(String username, String password) { + createUser(username, password, false, "xml", "user1@scm-manager.org"); } - public static void createUser(String username, String password, boolean isAdmin, String type) { + public static void createUser(String username, String password, boolean isAdmin, String type, final String email) { LOG.info("create user with username: {}", username); String admin = isAdmin ? "true" : "false"; given(VndMediaType.USER) @@ -61,7 +61,7 @@ public class TestData { .append(" \"admin\": ").append(admin).append(",\n") .append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n") .append(" \"displayName\": \"").append(username).append("\",\n") - .append(" \"mail\": \"user1@scm-manager.org\",\n") + .append(" \"mail\": \"" + email + "\",\n") .append(" \"name\": \"").append(username).append("\",\n") .append(" \"password\": \"").append(password).append("\",\n") .append(" \"type\": \"").append(type).append("\"\n") @@ -71,6 +71,16 @@ public class TestData { .statusCode(HttpStatus.SC_CREATED) ; } + public static void createGroup(String groupName, String desc) { + LOG.info("create group with group name: {} and description {}", groupName, desc); + given(VndMediaType.GROUP) + .when() + .content(getGroupJson(groupName,desc)) + .post(getGroupsUrl()) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); @@ -193,28 +203,31 @@ public class TestData { return JSON_BUILDER .add("contact", "zaphod.beeblebrox@hitchhiker.com") .add("description", "Heart of Gold") - .add("name", "HeartOfGold-" + repositoryType) + .add("name", getDefaultRepoName(repositoryType)) .add("archived", false) .add("type", repositoryType) .build().toString(); } - public static URI getMeUrl() { - return RestUtil.createResourceUrl("me/"); + public static String getDefaultRepoName(String repositoryType) { + return "HeartOfGold-" + repositoryType; + } + public static String getGroupJson(String groupname , String desc) { + return JSON_BUILDER + .add("name", groupname) + .add("description", desc) + .build().toString(); + } + + public static URI getGroupsUrl() { + return RestUtil.createResourceUrl("groups/"); } public static URI getUsersUrl() { return RestUtil.createResourceUrl("users/"); - } - public static URI getUserUrl(String username) { - return getUsersUrl().resolve(username); - - } - - public static String createPasswordChangeJson(String oldPassword, String newPassword) { return JSON_BUILDER .add("oldPassword", oldPassword) @@ -225,4 +238,5 @@ public class TestData { public static void main(String[] args) { cleanup(); } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java new file mode 100644 index 0000000000..a1120adda4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.plugin.Extension; +import sonia.scm.web.JsonEnricherBase; +import sonia.scm.web.JsonEnricherContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.INDEX; + +@Extension +public class GitConfigInIndexResource extends JsonEnricherBase { + + private final Provider scmPathInfoStore; + + @Inject + public GitConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) { + String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java index 6936c51269..2275fbcfd0 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitChangesetConverter.java @@ -39,7 +39,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; @@ -51,8 +50,8 @@ import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -130,27 +129,9 @@ public class GitChangesetConverter implements Closeable * * @throws IOException */ - public Changeset createChangeset(RevCommit commit) throws IOException + public Changeset createChangeset(RevCommit commit) { - List branches = Lists.newArrayList(); - Set refs = repository.getAllRefsByPeeledObjectId().get(commit.getId()); - - if (Util.isNotEmpty(refs)) - { - - for (Ref ref : refs) - { - String branch = GitUtil.getBranch(ref); - - if (branch != null) - { - branches.add(branch); - } - } - - } - - return createChangeset(commit, branches); + return createChangeset(commit, Collections.emptyList()); } /** @@ -165,7 +146,6 @@ public class GitChangesetConverter implements Closeable * @throws IOException */ public Changeset createChangeset(RevCommit commit, String branch) - throws IOException { return createChangeset(commit, Lists.newArrayList(branch)); } @@ -183,7 +163,6 @@ public class GitChangesetConverter implements Closeable * @throws IOException */ public Changeset createChangeset(RevCommit commit, List branches) - throws IOException { String id = commit.getId().name(); List parentList = null; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java index 7e145f2dd9..13340a20e7 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitUtil.java @@ -63,8 +63,11 @@ import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import static java.util.Optional.of; + //~--- JDK imports ------------------------------------------------------------ /** @@ -345,12 +348,11 @@ public final class GitUtil * * @throws IOException */ - public static ObjectId getBranchId(org.eclipse.jgit.lib.Repository repo, + public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo, String branchName) throws IOException { - ObjectId branchId = null; - + Ref ref = null; if (!branchName.startsWith(REF_HEAD)) { branchName = PREFIX_HEADS.concat(branchName); @@ -360,24 +362,19 @@ public final class GitUtil try { - Ref ref = repo.findRef(branchName); + ref = repo.findRef(branchName); - if (ref != null) - { - branchId = ref.getObjectId(); - } - else if (logger.isWarnEnabled()) + if (ref == null) { logger.warn("could not find branch for {}", branchName); } - } catch (IOException ex) { logger.warn("error occured during resolve of branch id", ex); } - return branchId; + return ref; } /** @@ -499,68 +496,48 @@ public final class GitUtil return ref; } - /** - * Method description - * - * - * @param repo - * - * @return - * - * @throws IOException - */ - public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) - throws IOException - { - ObjectId id = null; - String head = null; - Map refs = repo.getAllRefs(); + public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) { + return getRepositoryHeadRef(repo).map(Ref::getObjectId).orElse(null); + } - for (Map.Entry e : refs.entrySet()) - { - String key = e.getKey(); + public static Optional getRepositoryHeadRef(org.eclipse.jgit.lib.Repository repo) { + Optional foundRef = findMostAppropriateHead(repo.getAllRefs()); - if (REF_HEAD.equals(key)) - { - head = REF_HEAD; - id = e.getValue().getObjectId(); - - break; - } - else if (key.startsWith(REF_HEAD_PREFIX)) - { - id = e.getValue().getObjectId(); - head = key.substring(REF_HEAD_PREFIX.length()); - - if (REF_MASTER.equals(head)) - { - break; - } + if (foundRef.isPresent()) { + if (logger.isDebugEnabled()) { + logger.debug("use {}:{} as repository head for directory {}", + foundRef.map(GitUtil::getBranch).orElse(null), + foundRef.map(Ref::getObjectId).map(ObjectId::name).orElse(null), + repo.getDirectory()); } + } else { + logger.warn("could not find repository head in directory {}", repo.getDirectory()); } - if (id == null) - { - id = repo.resolve(Constants.HEAD); + return foundRef; + } + + private static Optional findMostAppropriateHead(Map refs) { + Ref refHead = refs.get(REF_HEAD); + if (refHead != null && refHead.isSymbolic() && isBranch(refHead.getTarget().getName())) { + return of(refHead.getTarget()); } - if (logger.isDebugEnabled()) - { - if ((head != null) && (id != null)) - { - logger.debug("use {}:{} as repository head", head, id.name()); - } - else if (id != null) - { - logger.debug("use {} as repository head", id.name()); - } - else - { - logger.warn("could not find repository head"); - } + Ref master = refs.get(REF_HEAD_PREFIX + REF_MASTER); + if (master != null) { + return of(master); } - return id; + Ref develop = refs.get(REF_HEAD_PREFIX + "develop"); + if (develop != null) { + return of(develop); + } + + return refs.entrySet() + .stream() + .filter(e -> e.getKey().startsWith(REF_HEAD_PREFIX)) + .map(Map.Entry::getValue) + .findFirst(); } /** @@ -648,7 +625,7 @@ public final class GitUtil return tagName; } - + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index d098c30b4e..2970bbd627 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -34,19 +34,20 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import org.eclipse.jgit.lib.Repository; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitConstants; import sonia.scm.repository.GitUtil; +import java.io.IOException; +import java.util.Optional; + +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -97,27 +98,29 @@ public class AbstractGitCommand } return commit; } - - protected ObjectId getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { - ObjectId head; - if ( Strings.isNullOrEmpty(requestedBranch) ) { - head = getDefaultBranch(gitRepository); - } else { - head = GitUtil.getBranchId(gitRepository, requestedBranch); - } - return head; - } - + protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException { - ObjectId head; - String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH); - if (!Strings.isNullOrEmpty(defaultBranchName)) { - head = GitUtil.getBranchId(gitRepository, defaultBranchName); + Ref ref = getBranchOrDefault(gitRepository, null); + if (ref == null) { + return null; } else { - logger.trace("no default branch configured, use repository head as default"); - head = GitUtil.getRepositoryHead(gitRepository); + return ref.getObjectId(); + } + } + + protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException { + if ( Strings.isNullOrEmpty(requestedBranch) ) { + String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH); + if (!Strings.isNullOrEmpty(defaultBranchName)) { + return GitUtil.getBranchId(gitRepository, defaultBranchName); + } else { + logger.trace("no default branch configured, use repository head as default"); + Optional repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository); + return repositoryHeadRef.orElse(null); + } + } else { + return GitUtil.getBranchId(gitRepository, requestedBranch); } - return head; } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java index beb79cb921..4e9261f517 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLogCommand.java @@ -39,6 +39,7 @@ import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -170,8 +171,8 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand GitChangesetConverter converter = null; RevWalk revWalk = null; - try (org.eclipse.jgit.lib.Repository gr = open()) { - if (!gr.getAllRefs().isEmpty()) { + try (org.eclipse.jgit.lib.Repository repository = open()) { + if (!repository.getAllRefs().isEmpty()) { int counter = 0; int start = request.getPagingStart(); @@ -188,18 +189,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand ObjectId startId = null; if (!Strings.isNullOrEmpty(request.getStartChangeset())) { - startId = gr.resolve(request.getStartChangeset()); + startId = repository.resolve(request.getStartChangeset()); } ObjectId endId = null; if (!Strings.isNullOrEmpty(request.getEndChangeset())) { - endId = gr.resolve(request.getEndChangeset()); + endId = repository.resolve(request.getEndChangeset()); } - revWalk = new RevWalk(gr); + revWalk = new RevWalk(repository); - converter = new GitChangesetConverter(gr, revWalk); + converter = new GitChangesetConverter(repository, revWalk); if (!Strings.isNullOrEmpty(request.getPath())) { revWalk.setTreeFilter( @@ -207,13 +208,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); } - ObjectId head = getBranchOrDefault(gr, request.getBranch()); + Ref branch = getBranchOrDefault(repository,request.getBranch()); - if (head != null) { + if (branch != null) { if (startId != null) { revWalk.markStart(revWalk.lookupCommit(startId)); } else { - revWalk.markStart(revWalk.lookupCommit(head)); + revWalk.markStart(revWalk.lookupCommit(branch.getObjectId())); } Iterator iterator = revWalk.iterator(); @@ -234,10 +235,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand } } - changesets = new ChangesetPagingResult(counter, changesetList); + if (branch != null) { + changesets = new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName())); + } else { + changesets = new ChangesetPagingResult(counter, changesetList); + } } else if (logger.isWarnEnabled()) { logger.warn("the repository {} seems to be empty", - repository.getName()); + this.repository.getName()); changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java new file mode 100644 index 0000000000..665be19788 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class GitConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final GitConfigInIndexResource gitConfigInIndexResource; + + public GitConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index d6e6ac98d8..78db8ae686 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -1,3 +1,4 @@ + /** * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. @@ -33,14 +34,17 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.io.Files; import org.junit.Test; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitConstants; import sonia.scm.repository.Modifications; +import java.io.File; +import java.io.IOException; + +import static java.nio.charset.Charset.defaultCharset; import static org.hamcrest.Matchers.contains; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -48,8 +52,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -//~--- JDK imports ------------------------------------------------------------ - /** * Unit tests for {@link GitLogCommand}. * @@ -72,6 +74,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId()); assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId()); + assertEquals("master", result.getBranchName()); + assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty())); // set default branch and fetch again repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); @@ -79,10 +83,12 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase result = createCommand().getChangesets(new LogCommandRequest()); assertNotNull(result); + assertEquals("test-branch", result.getBranchName()); assertEquals(3, result.getTotal()); assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getChangesets().get(0).getId()); assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId()); + assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty())); } @Test @@ -210,6 +216,32 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId()); } + @Test + public void shouldFindDefaultBranchFromHEAD() throws Exception { + setRepositoryHeadReference("ref: refs/heads/test-branch"); + + ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest()); + + assertEquals("test-branch", changesets.getBranchName()); + } + + @Test + public void shouldFindMasterBranchWhenHEADisNoRef() throws Exception { + setRepositoryHeadReference("592d797cd36432e591416e8b2b98154f4f163411"); + + ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest()); + + assertEquals("master", changesets.getBranchName()); + } + + private void setRepositoryHeadReference(String s) throws IOException { + Files.write(s, repositoryHeadReferenceFile(), defaultCharset()); + } + + private File repositoryHeadReferenceFile() { + return new File(repositoryDirectory, "HEAD"); + } + private GitLogCommand createCommand() { return new GitLogCommand(createContext(), repository); diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini index 36226edd7d..5d30a000f2 100644 --- a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -2,8 +2,10 @@ readOnly = secret, reader writeOnly = secret, writer readWrite = secret, readerWriter +admin = secret, admin [roles] reader = configuration:read:git writer = configuration:write:git readerWriter = configuration:*:git +admin = * diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java new file mode 100644 index 0000000000..3de79b2f81 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.plugin.Extension; +import sonia.scm.web.JsonEnricherBase; +import sonia.scm.web.JsonEnricherContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.INDEX; + +@Extension +public class HgConfigInIndexResource extends JsonEnricherBase { + + private final Provider scmPathInfoStore; + + @Inject + public HgConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) { + String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java index 68d6913962..e9de7f7471 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java @@ -132,7 +132,11 @@ public class HgLogCommand extends AbstractCommand implements LogCommand List changesets = on(repository).rev(start + ":" + end).execute(); - result = new ChangesetPagingResult(total, changesets); + if (request.getBranch() == null) { + result = new ChangesetPagingResult(total, changesets); + } else { + result = new ChangesetPagingResult(total, changesets, request.getBranch()); + } } else { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java index 6466eb6d11..89164a8d80 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java @@ -216,10 +216,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand String branch = in.textUpTo('\n'); - if (!BRANCH_DEFAULT.equals(branch)) - { - changeset.getBranches().add(branch); - } + changeset.getBranches().add(branch); String p1 = readId(in, changeset, PROPERTY_PARENT1_REVISION); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java new file mode 100644 index 0000000000..27ab74932c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class HgConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final HgConfigInIndexResource hgConfigInIndexResource; + + public HgConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java index 99e9fc191a..29fc46ed57 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java @@ -88,6 +88,21 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase result.getChangesets().get(2).getId()); } + @Test + public void testGetDefaultBranchInfo() { + LogCommandRequest request = new LogCommandRequest(); + + request.setPath("a.txt"); + + ChangesetPagingResult result = createComamnd().getChangesets(request); + + assertNotNull(result); + assertEquals(1, + result.getChangesets().get(0).getBranches().size()); + assertEquals("default", + result.getChangesets().get(0).getBranches().get(0)); + } + @Test public void testGetAllWithLimit() { LogCommandRequest request = new LogCommandRequest(); diff --git a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini index fc08bb83ac..d8083a04c9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -2,8 +2,10 @@ readOnly = secret, reader writeOnly = secret, writer readWrite = secret, readerWriter +admin = secret, admin [roles] reader = configuration:read:hg writer = configuration:write:hg readerWriter = configuration:*:hg +admin = * diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java new file mode 100644 index 0000000000..5ee1de3169 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.plugin.Extension; +import sonia.scm.web.JsonEnricherBase; +import sonia.scm.web.JsonEnricherContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.INDEX; + +@Extension +public class SvnConfigInIndexResource extends JsonEnricherBase { + + private final Provider scmPathInfoStore; + + @Inject + public SvnConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) { + String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java index 7cc78180ff..4352299ed5 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java @@ -32,122 +32,45 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.filter.GZipFilter; +import sonia.scm.filter.GZipFilterConfig; +import sonia.scm.filter.GZipResponseWrapper; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.util.WebUtil; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet -{ +class SvnGZipFilter implements ScmProviderHttpServlet { private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class); private final SvnRepositoryHandler handler; private final ScmProviderHttpServlet delegate; - //~--- constructors --------------------------------------------------------- + private GZipFilterConfig config = new GZipFilterConfig(); - /** - * Constructs ... - * - * - * @param handler - */ - public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) - { + SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) { this.handler = handler; this.delegate = delegate; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param filterConfig - * - * @throws ServletException - */ - @Override - public void init(FilterConfig filterConfig) throws ServletException - { - super.init(filterConfig); - getConfig().setBufferResponse(false); - } - - /** - * Method description - * - * - * @param request - * @param response - * @param chain - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) - throws IOException, ServletException - { - if (handler.getConfig().isEnabledGZip()) - { - if (logger.isTraceEnabled()) - { - logger.trace("encode svn request with gzip"); - } - - super.doFilter(request, response, chain); - } - else - { - if (logger.isTraceEnabled()) - { - logger.trace("skip gzip encoding"); - } - - chain.doFilter(request, response); - } + config.setBufferResponse(false); } @Override public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - if (handler.getConfig().isEnabledGZip()) - { - if (logger.isTraceEnabled()) - { - logger.trace("encode svn request with gzip"); - } - - super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository)); - } - else - { - if (logger.isTraceEnabled()) - { - logger.trace("skip gzip encoding"); - } - + if (handler.getConfig().isEnabledGZip() && WebUtil.isGzipSupported(request)) { + logger.trace("compress svn response with gzip"); + GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, config); + delegate.service(request, wrappedResponse, repository); + wrappedResponse.finishResponse(); + } else { + logger.trace("skip gzip encoding"); delegate.service(request, response, repository); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java new file mode 100644 index 0000000000..8b87b57c6c --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class SvnConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final SvnConfigInIndexResource svnConfigInIndexResource; + + public SvnConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini index 7e4233b540..fe84723e0a 100644 --- a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -2,8 +2,10 @@ readOnly = secret, reader writeOnly = secret, writer readWrite = secret, readerWriter +admin = secret, admin [roles] reader = configuration:read:svn writer = configuration:write:svn readerWriter = configuration:*:svn +admin = * diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json index 6fe782506e..527acbd9dc 100644 --- a/scm-ui-components/package.json +++ b/scm-ui-components/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "bootstrap": "lerna bootstrap", - "link": "lerna exec -- yarn link" + "link": "lerna exec -- yarn link", + "unlink": "lerna exec --no-bail -- yarn unlink || true" }, "devDependencies": { "lerna": "^3.2.1" diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 78d03e757a..0bc4c694a7 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -19,7 +19,8 @@ "flow-bin": "^0.79.1", "flow-typed": "^2.5.1", "jest": "^23.5.0", - "raf": "^3.4.0" + "raf": "^3.4.0", + "react-router-enzyme-context": "^1.2.0" }, "dependencies": { "classnames": "^2.2.6", diff --git a/scm-ui-components/packages/ui-components/src/Help.js b/scm-ui-components/packages/ui-components/src/Help.js new file mode 100644 index 0000000000..965d16f145 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/Help.js @@ -0,0 +1,39 @@ +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; + +const styles = { + img: { + display: "block" + }, + q: { + float: "left", + paddingLeft: "3px", + float: "right" + } +}; + +type Props = { + message: string, + classes: any +}; + +class Help extends React.Component { + render() { + const { message, classes } = this.props; + const multiline = message.length > 60 ? "is-tooltip-multiline" : ""; + return ( +
+ +
+ ); + } +} + +export default injectSheet(styles)(Help); diff --git a/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js b/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js new file mode 100644 index 0000000000..b5d049e68d --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js @@ -0,0 +1,46 @@ +//@flow +import React from "react"; +import { Help } from "./index"; + +type Props = { + label: string, + helpText?: string +}; + +class LabelWithHelpIcon extends React.Component { + renderLabel = () => { + const label = this.props.label; + if (label) { + return ; + } + return ""; + }; + + renderHelp = () => { + const helpText = this.props.helpText; + if (helpText) { + return ( +
+ +
+ ); + } else return null; + }; + + renderLabelWithHelpIcon = () => { + if (this.props.label) { + return ( +
+
{this.renderLabel()}
+ {this.renderHelp()} +
+ ); + } else return null; + }; + + render() { + return this.renderLabelWithHelpIcon(); + } +} + +export default LabelWithHelpIcon; diff --git a/scm-ui-components/packages/ui-components/src/Paginator.test.js b/scm-ui-components/packages/ui-components/src/Paginator.test.js index 929836f773..f3ac840f67 100644 --- a/scm-ui-components/packages/ui-components/src/Paginator.test.js +++ b/scm-ui-components/packages/ui-components/src/Paginator.test.js @@ -3,10 +3,13 @@ import React from "react"; import { mount, shallow } from "enzyme"; import "./tests/enzyme"; import "./tests/i18n"; - +import ReactRouterEnzymeContext from "react-router-enzyme-context"; import Paginator from "./Paginator"; describe("paginator rendering tests", () => { + + const options = new ReactRouterEnzymeContext(); + const dummyLink = { href: "https://dummy" }; @@ -18,7 +21,10 @@ describe("paginator rendering tests", () => { _links: {} }; - const paginator = shallow(); + const paginator = shallow( + , + options.get() + ); const buttons = paginator.find("Button"); expect(buttons.length).toBe(7); for (let button of buttons) { @@ -37,7 +43,10 @@ describe("paginator rendering tests", () => { } }; - const paginator = shallow(); + const paginator = shallow( + , + options.get() + ); const buttons = paginator.find("Button"); expect(buttons.length).toBe(5); @@ -73,7 +82,10 @@ describe("paginator rendering tests", () => { } }; - const paginator = shallow(); + const paginator = shallow( + , + options.get() + ); const buttons = paginator.find("Button"); expect(buttons.length).toBe(6); @@ -112,7 +124,10 @@ describe("paginator rendering tests", () => { } }; - const paginator = shallow(); + const paginator = shallow( + , + options.get() + ); const buttons = paginator.find("Button"); expect(buttons.length).toBe(5); @@ -148,7 +163,10 @@ describe("paginator rendering tests", () => { } }; - const paginator = shallow(); + const paginator = shallow( + , + options.get() + ); const buttons = paginator.find("Button"); expect(buttons.length).toBe(6); @@ -189,7 +207,10 @@ describe("paginator rendering tests", () => { } }; - const paginator = shallow(); + const paginator = shallow( + , + options.get() + ); const buttons = paginator.find("Button"); expect(buttons.length).toBe(7); @@ -244,7 +265,8 @@ describe("paginator rendering tests", () => { }; const paginator = mount( - + , + options.get() ); paginator.find("Button.pagination-previous").simulate("click"); diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js index 7bf3232260..0b57abeada 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.js @@ -32,7 +32,7 @@ export function createUrl(url: string) { if (url.indexOf("/") !== 0) { urlWithStartingSlash = "/" + urlWithStartingSlash; } - return `${contextPath}/api/rest/v2${urlWithStartingSlash}`; + return `${contextPath}/api/v2${urlWithStartingSlash}`; } class ApiClient { diff --git a/scm-ui-components/packages/ui-components/src/apiclient.test.js b/scm-ui-components/packages/ui-components/src/apiclient.test.js index 7bbb3b0119..deb22a3b54 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.test.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.test.js @@ -9,7 +9,7 @@ describe("create url", () => { }); it("should add prefix for api", () => { - expect(createUrl("/users")).toBe("/api/rest/v2/users"); - expect(createUrl("users")).toBe("/api/rest/v2/users"); + expect(createUrl("/users")).toBe("/api/v2/users"); + expect(createUrl("users")).toBe("/api/v2/users"); }); }); diff --git a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js index 1770e07807..e5c04eb613 100644 --- a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js +++ b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js @@ -9,7 +9,8 @@ type Props = { disabled: boolean, buttonLabel: string, fieldLabel: string, - errorMessage: string + errorMessage: string, + helpText?: string }; type State = { @@ -25,7 +26,13 @@ class AddEntryToTableField extends React.Component { } render() { - const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props; + const { + disabled, + buttonLabel, + fieldLabel, + errorMessage, + helpText + } = this.props; return (
{ value={this.state.entryToAdd} onReturnPressed={this.appendEntry} disabled={disabled} + helpText={helpText} /> void, - disabled?: boolean + disabled?: boolean, + helpText?: string }; class Checkbox extends React.Component { onCheckboxChange = (event: SyntheticInputEvent) => { @@ -14,9 +16,20 @@ class Checkbox extends React.Component { } }; + renderHelp = () => { + const helpText = this.props.helpText; + if (helpText) { + return ( +
+ +
+ ); + } else return null; + }; + render() { return ( -
+
+ {this.renderHelp()}
); } diff --git a/scm-ui-components/packages/ui-components/src/forms/InputField.js b/scm-ui-components/packages/ui-components/src/forms/InputField.js index 6f87683939..79b71298f8 100644 --- a/scm-ui-components/packages/ui-components/src/forms/InputField.js +++ b/scm-ui-components/packages/ui-components/src/forms/InputField.js @@ -1,6 +1,7 @@ //@flow import React from "react"; import classNames from "classnames"; +import { LabelWithHelpIcon } from "../index"; type Props = { label?: string, @@ -12,7 +13,8 @@ type Props = { onReturnPressed?: () => void, validationError: boolean, errorMessage: string, - disabled?: boolean + disabled?: boolean, + helpText?: string }; class InputField extends React.Component { @@ -33,15 +35,6 @@ class InputField extends React.Component { this.props.onChange(event.target.value); }; - renderLabel = () => { - const label = this.props.label; - if (label) { - return ; - } - return ""; - }; - - handleKeyPress = (event: SyntheticKeyboardEvent) => { const onReturnPressed = this.props.onReturnPressed; if (!onReturnPressed) { @@ -60,7 +53,9 @@ class InputField extends React.Component { value, validationError, errorMessage, - disabled + disabled, + label, + helpText } = this.props; const errorView = validationError ? "is-danger" : ""; const helper = validationError ? ( @@ -70,7 +65,7 @@ class InputField extends React.Component { ); return (
- {this.renderLabel()} +
{ diff --git a/scm-ui-components/packages/ui-components/src/forms/Select.js b/scm-ui-components/packages/ui-components/src/forms/Select.js index 184359cc11..880b375999 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Select.js +++ b/scm-ui-components/packages/ui-components/src/forms/Select.js @@ -1,5 +1,7 @@ //@flow import React from "react"; +import classNames from "classnames"; +import { LabelWithHelpIcon } from "../index"; export type SelectItem = { value: string, @@ -10,7 +12,9 @@ type Props = { label?: string, options: SelectItem[], value?: SelectItem, - onChange: string => void + onChange: string => void, + loading?: boolean, + helpText?: string }; class Select extends React.Component { @@ -28,21 +32,18 @@ class Select extends React.Component { this.props.onChange(event.target.value); }; - renderLabel = () => { - const label = this.props.label; - if (label) { - return ; - } - return ""; - }; - render() { - const { options, value } = this.props; + const { options, value, label, helpText, loading } = this.props; + const loadingClass = loading ? "is-loading" : ""; + return (
- {this.renderLabel()} -
+ +