diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java index 422611e4e8..18ede493f6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java +++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java @@ -33,11 +33,9 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Objects; import sonia.scm.BasicPropertiesAware; -import sonia.scm.Validateable; +import sonia.scm.ModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -45,12 +43,10 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.List; -//~--- JDK imports ------------------------------------------------------------ /** * Represents a changeset/commit of a repository. @@ -59,43 +55,58 @@ import java.util.List; */ @XmlRootElement(name = "changeset") @XmlAccessorType(XmlAccessType.FIELD) -public class Changeset extends BasicPropertiesAware - implements Validateable, Serializable -{ +public class Changeset extends BasicPropertiesAware implements ModelObject { - /** Field description */ private static final long serialVersionUID = -8373308448928993039L; - //~--- constructors --------------------------------------------------------- + /** + * The author of the changeset + */ + private Person author; /** - * Constructs a new instance of changeset. - * + * The name of the branches on which the changeset was committed. */ + private List branches; + + /** + * The date when the changeset was committed + */ + private Long date; + + /** + * The text of the changeset description + */ + private String description; + + /** + * The changeset identification string + */ + private String id; + + /** + * List of files changed by this changeset + */ + @XmlElement(name = "modifications") + private Modifications modifications; + + /** + * parent changeset ids + */ + private List parents; + + /** + * The tags associated with the changeset + */ + private List tags; + public Changeset() {} - /** - * Constructs a new instance of changeset. - * - * - * @param id id of the changeset - * @param date date of the changeset - * @param author author of the changeset - */ public Changeset(String id, Long date, Person author) { this(id, date, author, null); } - /** - * Constructs a new instance of changeset. - * - * - * @param id id of the changeset - * @param date date of the changeset - * @param author author of the changeset - * @param description description of the changeset - */ public Changeset(String id, Long date, Person author, String description) { this.id = id; @@ -104,16 +115,6 @@ public class Changeset extends BasicPropertiesAware this.description = description; } - //~--- methods -------------------------------------------------------------- - - /** - * {@inheritDoc} - * - * - * @param obj - * - * @return - */ @Override public boolean equals(Object obj) { @@ -122,15 +123,14 @@ public class Changeset extends BasicPropertiesAware return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } final Changeset other = (Changeset) obj; //J- - return Objects.equal(id, other.id) + return Objects.equal(id, other.id) && Objects.equal(date, other.date) && Objects.equal(author, other.author) && Objects.equal(description, other.description) @@ -192,7 +192,21 @@ public class Changeset extends BasicPropertiesAware return out.toString(); } - //~--- get methods ---------------------------------------------------------- + + /** + * Returns a timestamp of the creation date of the {@link Changeset}. + * + * @return a timestamp of the creation date of the {@link Changeset} + */ + public Long getCreationDate() { + return getDate(); + } + + @Override + public void setCreationDate(Long timestamp) { + this.setDate(timestamp); + } + /** * Returns the author of the changeset. @@ -200,14 +214,13 @@ public class Changeset extends BasicPropertiesAware * * @return author of the changeset */ - public Person getAuthor() - { + public Person getAuthor() { return author; } /** - * Returns the branches of the changeset. In the most cases a changeset is - * only related to one branch, but in the case of receive hooks it is possible + * Returns the branches of the changeset. In the most cases a changeset is + * only related to one branch, but in the case of receive hooks it is possible * that a changeset is related to more than a branch. * * @@ -251,11 +264,27 @@ public class Changeset extends BasicPropertiesAware * * @return id of the changeset */ - public String getId() - { + @Override + public String getId() { return id; } + @Override + public void setLastModified(Long timestamp) { + throw new UnsupportedOperationException("changesets are immutable"); + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public String getType() { + return "Changeset"; + } + + /** * Returns the file modifications, which was done with this changeset. * @@ -318,8 +347,6 @@ public class Changeset extends BasicPropertiesAware && (date != null); } - //~--- set methods ---------------------------------------------------------- - /** * Sets the author of the changeset. * @@ -409,30 +436,4 @@ public class Changeset extends BasicPropertiesAware this.tags = tags; } - //~--- fields --------------------------------------------------------------- - - /** The author of the changeset */ - private Person author; - - /** The name of the branches on which the changeset was committed. */ - private List branches; - - /** The date when the changeset was committed */ - private Long date; - - /** The text of the changeset description */ - private String description; - - /** The changeset identification string */ - private String id; - - /** List of files changed by this changeset */ - @XmlElement(name = "modifications") - private Modifications modifications; - - /** parent changeset ids */ - private List parents; - - /** The tags associated with the changeset */ - private List tags; } diff --git a/scm-core/src/main/java/sonia/scm/repository/Person.java b/scm-core/src/main/java/sonia/scm/repository/Person.java index d5674e7ad0..8b5e66f61a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Person.java +++ b/scm-core/src/main/java/sonia/scm/repository/Person.java @@ -249,9 +249,11 @@ public class Person implements Validateable, Serializable //~--- fields --------------------------------------------------------------- - /** name of the person */ + /** mail address of the person */ private String mail; - /** mail address of the person */ + /** + * name of the person + */ private String name; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java index 4fed0a7b60..1f4defbd8b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java @@ -140,7 +140,7 @@ public final class BundleCommandBuilder throws IOException { checkNotNull(sink, "byte sink is required"); - logger.info("bundle {} to byte sink"); + logger.info("bundle {} to byte sink", sink); return bundleCommand.bundle(new BundleCommandRequest(sink)); } 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 569f47889b..73bcf10bd0 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -17,6 +17,8 @@ public class VndMediaType { public static final String GROUP = PREFIX + "group" + 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; + public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; 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 a7ee8da41d..a9139f18c8 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -14,7 +14,9 @@ import sonia.scm.repository.client.api.RepositoryClient; import java.io.File; import java.io.IOException; import java.util.Collection; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static java.lang.Thread.sleep; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertNotNull; @@ -124,4 +126,31 @@ public class RepositoryAccessITCase { .statusCode(HttpStatus.SC_OK) .body(equalTo("sub-a")); } + + @Test + public void shouldFindChangesets() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "b.txt", "b"); + + String changesetsUrl = given() + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.changesets.href"); + + List changesets = given() + .when() + .get(changesetsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_embedded.changesets.id"); + + assertThat(changesets).size().isBetween(2, 3); // svn has an implicit root revision '0' that is extra to the two commits + } } + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java index 7244a1f2bb..82e2919996 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitSubModuleParser.java @@ -1,19 +1,19 @@ /** * 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. + * 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. + * 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. - * + * 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 @@ -24,96 +24,49 @@ * 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.repository; -//~--- non-JDK imports -------------------------------------------------------- - import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import java.util.HashMap; import java.util.Map; import java.util.Scanner; /** - * * @author Sebastian Sdorra */ -public final class GitSubModuleParser -{ +public final class GitSubModuleParser { - /** - * Constructs ... - * - */ - private GitSubModuleParser() {} + private GitSubModuleParser() { + } - //~--- methods -------------------------------------------------------------- - - /** - * //~--- methods -------------------------------------------------------------- - * - * - * Method description - * - * - * @param content - * - * @return - */ - public static Map parse(String content) - { - Map subRepositories = new HashMap(); - Scanner scanner = new Scanner(content); - SubRepository repository = null; - - while (scanner.hasNextLine()) - { - String line = scanner.nextLine(); - - if (Util.isNotEmpty(line)) - { - line = line.trim(); - - if (line.startsWith("[") && line.endsWith("]")) - { - repository = new SubRepository(); - } - else if (line.startsWith("path")) - { - subRepositories.put(getValue(line), repository); - } - else if (line.startsWith("url")) - { - repository.setRepositoryUrl(getValue(line)); + public static Map parse(String content) { + Map subRepositories = new HashMap<>(); + try (Scanner scanner = new Scanner(content)) { + SubRepository repository = null; + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (Util.isNotEmpty(line)) { + line = line.trim(); + if (line.startsWith("[") && line.endsWith("]")) { + repository = new SubRepository(); + } else if (line.startsWith("path")) { + subRepositories.put(getValue(line), repository); + } else if (line.startsWith("url")) { + repository.setRepositoryUrl(getValue(line)); + } } } } - return subRepositories; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param line - * - * @return - */ - private static String getValue(String line) - { + private static String getValue(String line) { return line.split("=")[1].trim(); } } 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 65a6f9daf8..beb79cb921 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 @@ -37,6 +37,7 @@ package sonia.scm.repository.spi; 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.Repository; import org.eclipse.jgit.revwalk.RevCommit; @@ -51,6 +52,7 @@ import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.util.IOUtil; import java.io.IOException; @@ -156,15 +158,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand * @return * * @throws IOException - * @throws RepositoryException */ @Override @SuppressWarnings("unchecked") - public ChangesetPagingResult getChangesets(LogCommandRequest request) - throws IOException - { - if (logger.isDebugEnabled()) - { + public ChangesetPagingResult getChangesets(LogCommandRequest request) throws RevisionNotFoundException { + if (logger.isDebugEnabled()) { logger.debug("fetch changesets for request: {}", request); } @@ -172,17 +170,13 @@ 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 gr = open()) { + if (!gr.getAllRefs().isEmpty()) { int counter = 0; int start = request.getPagingStart(); - if (start < 0) - { - if (logger.isErrorEnabled()) - { + if (start < 0) { + if (logger.isErrorEnabled()) { logger.error("start parameter is negative, reset to 0"); } @@ -193,15 +187,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand int limit = request.getPagingLimit(); ObjectId startId = null; - if (!Strings.isNullOrEmpty(request.getStartChangeset())) - { + if (!Strings.isNullOrEmpty(request.getStartChangeset())) { startId = gr.resolve(request.getStartChangeset()); } ObjectId endId = null; - if (!Strings.isNullOrEmpty(request.getEndChangeset())) - { + if (!Strings.isNullOrEmpty(request.getEndChangeset())) { endId = gr.resolve(request.getEndChangeset()); } @@ -209,57 +201,51 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand converter = new GitChangesetConverter(gr, revWalk); - if (!Strings.isNullOrEmpty(request.getPath())) - { + if (!Strings.isNullOrEmpty(request.getPath())) { revWalk.setTreeFilter( AndTreeFilter.create( PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); } - + ObjectId head = getBranchOrDefault(gr, request.getBranch()); - if (head != null) - { - if (startId != null) - { + if (head != null) { + if (startId != null) { revWalk.markStart(revWalk.lookupCommit(startId)); - } - else - { + } else { revWalk.markStart(revWalk.lookupCommit(head)); } Iterator iterator = revWalk.iterator(); - while (iterator.hasNext()) - { + while (iterator.hasNext()) { RevCommit commit = iterator.next(); if ((counter >= start) - && ((limit < 0) || (counter < start + limit))) - { + && ((limit < 0) || (counter < start + limit))) { changesetList.add(converter.createChangeset(commit)); } counter++; - if ((endId != null) && commit.getId().equals(endId)) - { + if ((endId != null) && commit.getId().equals(endId)) { break; } } } changesets = new ChangesetPagingResult(counter, changesetList); - } - else if (logger.isWarnEnabled()) - { + } else if (logger.isWarnEnabled()) { logger.warn("the repository {} seems to be empty", repository.getName()); changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST); } } + catch (MissingObjectException e) + { + throw new RevisionNotFoundException(e.getObjectId().name()); + } catch (Exception ex) { throw new InternalRepositoryException("could not create change log", ex); 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 1d78485ae3..51df1298e5 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 @@ -35,15 +35,12 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- -import org.eclipse.jgit.api.errors.GitAPIException; 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.IOException; - import static org.hamcrest.Matchers.contains; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -55,7 +52,7 @@ import static org.junit.Assert.assertTrue; /** * Unit tests for {@link GitLogCommand}. - * + * * @author Sebastian Sdorra */ public class GitLogCommandTest extends AbstractGitCommandTestBase @@ -63,13 +60,9 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase /** * Tests log command with the usage of a default branch. - * - * @throws IOException - * @throws GitAPIException - * @ */ @Test - public void testGetDefaultBranch() throws IOException, GitAPIException { + public void testGetDefaultBranch() throws Exception { // without default branch, the repository head should be used ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -79,10 +72,10 @@ 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()); - + // set default branch and fetch again repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); - + result = createCommand().getChangesets(new LogCommandRequest()); assertNotNull(result); @@ -91,16 +84,9 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId()); } - - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ + @Test - public void testGetAll() throws IOException + public void testGetAll() throws Exception { ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); @@ -110,15 +96,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals(4, result.getChangesets().size()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllByPath() throws IOException + public void testGetAllByPath() throws Exception { LogCommandRequest request = new LogCommandRequest(); @@ -133,15 +112,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(1).getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithLimit() throws IOException + public void testGetAllWithLimit() throws Exception { LogCommandRequest request = new LogCommandRequest(); @@ -164,15 +136,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", c2.getId()); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetAllWithPaging() throws IOException + public void testGetAllWithPaging() throws Exception { LogCommandRequest request = new LogCommandRequest(); @@ -196,10 +161,6 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("592d797cd36432e591416e8b2b98154f4f163411", c2.getId()); } - /** - * Method description - * - */ @Test public void testGetCommit() { @@ -224,15 +185,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertThat(mods.getAdded(), contains("a.txt", "b.txt")); } - /** - * Method description - * - * - * @throws IOException - * @throws RepositoryException - */ @Test - public void testGetRange() throws IOException + public void testGetRange() throws Exception { LogCommandRequest request = new LogCommandRequest(); @@ -254,12 +208,6 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId()); } - /** - * Method description - * - * - * @return - */ private GitLogCommand createCommand() { return new GitLogCommand(createContext(), repository); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java index 4700d22841..b189a22d1c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java @@ -11,6 +11,8 @@ import sonia.scm.PageResult; import javax.inject.Inject; import java.util.EnumSet; import java.util.List; +import java.util.Optional; +import java.util.function.Function; import static com.damnhandy.uri.template.UriTemplate.fromTemplate; import static de.otto.edison.hal.Embedded.embeddedBuilder; @@ -19,25 +21,28 @@ import static de.otto.edison.hal.Links.linkingTo; import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging; import static java.util.stream.Collectors.toList; -abstract class BasicCollectionToDtoMapper { +abstract class BasicCollectionToDtoMapper> { private final String collectionName; - private final BaseMapper entityToDtoMapper; + + private final M entityToDtoMapper; @Inject - public BasicCollectionToDtoMapper(String collectionName, BaseMapper entityToDtoMapper) { + public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) { this.collectionName = collectionName; this.entityToDtoMapper = entityToDtoMapper; } - public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult) { - NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount()); - List dtos = pageResult.getEntities().stream().map(entityToDtoMapper::map).collect(toList()); + CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, String selfLink, Optional createLink) { + return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map); + } + CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, String selfLink, Optional createLink, Function mapper) { + NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount()); + List dtos = pageResult.getEntities().stream().map(mapper).collect(toList()); CollectionDto collectionDto = new CollectionDto( - createLinks(paging), - embedDtos(dtos) - ); + createLinks(paging, selfLink, createLink), + embedDtos(dtos)); collectionDto.setPage(pageNumber); collectionDto.setPageTotal(computePageTotal(pageSize, pageResult)); return collectionDto; @@ -51,26 +56,16 @@ abstract class BasicCollectionToDtoMapper createLink) { Links.Builder linksBuilder = linkingTo() .with(page.links( - fromTemplate(baseUrl + "{?page,pageSize}"), + fromTemplate(selfLink + "{?page,pageSize}"), EnumSet.allOf(PagingRel.class))); - if (isCreatePermitted()) { - linksBuilder.single(link("create", createCreateLink())); - } + createLink.ifPresent(link -> linksBuilder.single(link("create", link))); return linksBuilder.build(); } - abstract boolean isCreatePermitted(); - - abstract String createCreateLink(); - - abstract String createSelfLink(); - - private Embedded embedDtos(List dtos) { + private Embedded embedDtos(List dtos) { return embeddedBuilder() .with(collectionName, dtos) .build(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java index 9e574b3a7b..51ce9e6f5a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java @@ -26,8 +26,11 @@ public class BranchCollectionToDtoMapper { } public HalRepresentation map(String namespace, String name, Collection branches) { - List dtos = branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); - return new HalRepresentation(createLinks(namespace, name), embedDtos(dtos)); + return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches))); + } + + public List getBranchDtoList(String namespace, String name, Collection branches) { + return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); } private Links createLinks(String namespace, String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index d44a91d2c9..9763f11def 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -3,33 +3,43 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.PageResult; import sonia.scm.repository.Branches; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.CommandNotSupportedException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; public class BranchRootResource { - private final RepositoryServiceFactory servicefactory; + private final RepositoryServiceFactory serviceFactory; private final BranchToBranchDtoMapper branchToDtoMapper; private final BranchCollectionToDtoMapper branchCollectionToDtoMapper; + private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; + @Inject - public BranchRootResource(RepositoryServiceFactory servicefactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper) { - this.servicefactory = servicefactory; + public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) { + this.serviceFactory = serviceFactory; this.branchToDtoMapper = branchToDtoMapper; this.branchCollectionToDtoMapper = branchCollectionToDtoMapper; + this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper; } /** @@ -37,10 +47,9 @@ public class BranchRootResource { * * Note: This method requires "repository" privilege. * - * @param namespace the namespace of the repository - * @param name the name of the repository + * @param namespace the namespace of the repository + * @param name the name of the repository * @param branchName the name of the branch - * */ @GET @Path("{branch}") @@ -55,7 +64,7 @@ public class BranchRootResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException { - try (RepositoryService repositoryService = servicefactory.create(new NamespaceAndName(namespace, name))) { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Branches branches = repositoryService.getBranchesCommand().getBranches(); return branches.getBranches() .stream() @@ -74,8 +83,35 @@ public class BranchRootResource { @Path("{branch}/changesets/") @GET - public Response history(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) { - throw new UnsupportedOperationException(); + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response history(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("branch") String branchName, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setPagingStart(page) + .setPagingLimit(pageSize) + .setBranch(branchName) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build(); + } else { + return Response.ok().build(); + } + } } /** @@ -84,8 +120,7 @@ public class BranchRootResource { * Note: This method requires "repository" privilege. * * @param namespace the namespace of the repository - * @param name the name of the repository - * + * @param name the name of the repository */ @GET @Path("") @@ -100,7 +135,7 @@ public class BranchRootResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { - try (RepositoryService repositoryService = servicefactory.create(new NamespaceAndName(namespace, name))) { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { Branches branches = repositoryService.getBranchesCommand().getBranches(); return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build(); } catch (CommandNotSupportedException ex) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java new file mode 100644 index 0000000000..fcc4085486 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java @@ -0,0 +1,29 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; +import java.util.Optional; + +public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper { + + private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; + private final ResourceLinks resourceLinks; + + @Inject + public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { + super("changesets", changesetToChangesetDtoMapper); + this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; + this.resourceLinks = resourceLinks; + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository) { + return super.map(pageNumber, pageSize, pageResult, createSelfLink(repository), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + } + + private String createSelfLink(Repository repository) { + return resourceLinks.changeset().all(repository.getNamespace(), repository.getName()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java new file mode 100644 index 0000000000..162d5d6699 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java @@ -0,0 +1,49 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.Instant; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class ChangesetDto extends HalRepresentation { + + /** + * The changeset identification string + */ + private String id; + + /** + * The author of the changeset + */ + private PersonDto author; + + /** + * The date when the changeset was committed + */ + private Instant date; + + /** + * The text of the changeset description + */ + private String description; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation withEmbedded(String rel, List halRepresentations) { + return super.withEmbedded(rel, halRepresentations); + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java index 42ffbdd6c1..717b8d7198 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -1,25 +1,101 @@ package sonia.scm.api.v2.resources; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.io.IOException; + +@Slf4j public class ChangesetRootResource { + private final RepositoryServiceFactory serviceFactory; + + private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; + + private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; + + @Inject + public ChangesetRootResource(RepositoryServiceFactory serviceFactory, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper, ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper) { + this.serviceFactory = serviceFactory; + this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper; + this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; + } + @GET @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setPagingStart(page) + .setPagingLimit(pageSize) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build(); + } else { + return Response.ok().build(); + } + } } @GET - @Path("{revision}") - public Response get() { - throw new UnsupportedOperationException(); + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET) + @TypeHint(ChangesetDto.class) + @Path("{id}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("id") String id) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setStartChangeset(id) + .setEndChangeset(id) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null && changesets.getChangesets().size() == 1) { + return Response.ok(changesetToChangesetDtoMapper.map(changesets.getChangesets().get(0), repository)).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java new file mode 100644 index 0000000000..f047e3bbaa --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java @@ -0,0 +1,78 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Branch; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import javax.inject.Inject; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class ChangesetToChangesetDtoMapper extends BaseMapper { + + @Inject + private RepositoryServiceFactory serviceFactory; + + @Inject + private ResourceLinks resourceLinks; + + + @Inject + private BranchCollectionToDtoMapper branchCollectionToDtoMapper; + + @Inject + private ChangesetToParentDtoMapper changesetToParentDtoMapper; + + @Inject + private TagCollectionToDtoMapper tagCollectionToDtoMapper; + + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract ChangesetDto map(Changeset changeset, @Context Repository repository); + + + @AfterMapping + void appendLinks(Changeset source, @MappingTarget ChangesetDto target, @Context Repository repository) { + String namespace = repository.getNamespace(); + String name = repository.getName(); + + try (RepositoryService repositoryService = serviceFactory.create(repository)) { + if (repositoryService.isSupported(Command.TAGS)) { + target.withEmbedded("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, + getListOfObjects(source.getTags(), tagName -> new Tag(tagName, source.getId())))); + } + if (repositoryService.isSupported(Command.BRANCHES)) { + target.withEmbedded("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, + getListOfObjects(source.getBranches(), branchName -> new Branch(branchName, source.getId())))); + } + } + target.withEmbedded("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository))); + + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId())) + .single(link("diff", resourceLinks.diff().self(namespace, name, target.getId()))); + target.add(linksBuilder.build()); + } + + private List getListOfObjects(List list, Function mapFunction) { + return list + .stream() + .map(mapFunction) + .collect(Collectors.toList()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToParentDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToParentDtoMapper.java new file mode 100644 index 0000000000..611f5e6cbb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToParentDtoMapper.java @@ -0,0 +1,38 @@ +package sonia.scm.api.v2.resources; + + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class ChangesetToParentDtoMapper extends BaseMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract ParentChangesetDto map(Changeset changeset, @Context Repository repository); + + + @AfterMapping + void appendLinks(@MappingTarget ParentChangesetDto target, @Context Repository repository) { + String namespace = repository.getNamespace(); + String name = repository.getName(); + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId())) + .single(link("diff", resourceLinks.diff().self(namespace, name, target.getId()))); + target.add(linksBuilder.build()); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java new file mode 100644 index 0000000000..176a86dda7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -0,0 +1,27 @@ +package sonia.scm.api.v2.resources; + +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class DiffRootResource { + + + @GET + @Path("") + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc) { + throw new UnsupportedOperationException(); + } + + @GET + @Path("{id}") + public Response get(String id) { + throw new UnsupportedOperationException(); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapper.java index 415f04bfaf..1550e57ce2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapper.java @@ -1,11 +1,16 @@ package sonia.scm.api.v2.resources; +import sonia.scm.PageResult; import sonia.scm.group.Group; import sonia.scm.group.GroupPermissions; import javax.inject.Inject; +import java.util.Optional; -public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper { +import static java.util.Optional.empty; +import static java.util.Optional.of; + +public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ResourceLinks resourceLinks; @@ -15,18 +20,15 @@ public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper pageResult) { + return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink()); } - @Override - String createSelfLink() { + private Optional createCreateLink() { + return GroupPermissions.create().isPermitted() ? of(resourceLinks.groupCollection().create()): empty(); + } + + private String createSelfLink() { return resourceLinks.groupCollection().self(); } - - @Override - boolean isCreatePermitted() { - return GroupPermissions.create().isPermitted(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index a41bd2b041..9cadbfb6ff 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -28,6 +28,11 @@ public class MapperModule extends AbstractModule { bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); + bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass()); + bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); + + bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass()); + bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); // no mapstruct required diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ParentChangesetDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ParentChangesetDto.java new file mode 100644 index 0000000000..17dbc0df22 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ParentChangesetDto.java @@ -0,0 +1,27 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +public class ParentChangesetDto extends HalRepresentation { + + /** + * the id of the parent changeset + */ + private String id; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java new file mode 100644 index 0000000000..ce35b507c8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PersonDto.java @@ -0,0 +1,22 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class PersonDto { + + /** + * mail address of the person + */ + private String mail; + + /** + * name of the person + */ + private String name; + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java index a1cf0218e4..77674270d7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java @@ -1,13 +1,18 @@ package sonia.scm.api.v2.resources; +import sonia.scm.PageResult; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; import javax.inject.Inject; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") -public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper { +public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ResourceLinks resourceLinks; @@ -17,18 +22,15 @@ public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper< this.resourceLinks = resourceLinks; } - @Override - String createCreateLink() { - return resourceLinks.repositoryCollection().create(); + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult) { + return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink()); + } + + Optional createCreateLink() { + return RepositoryPermissions.create().isPermitted() ? of(resourceLinks.repositoryCollection().create()): empty(); } - @Override String createSelfLink() { return resourceLinks.repositoryCollection().self(); } - - @Override - boolean isCreatePermitted() { - return RepositoryPermissions.create().isPermitted(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index d2055b731d..c47fc4f3d1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -39,6 +39,7 @@ public class RepositoryResource { private final Provider sourceRootResource; private final Provider contentResource; private final Provider permissionRootResource; + private final Provider diffRootResource; @Inject public RepositoryResource( @@ -47,7 +48,9 @@ public class RepositoryResource { Provider tagRootResource, Provider branchRootResource, Provider changesetRootResource, - Provider sourceRootResource, Provider contentResource, Provider permissionRootResource) { + Provider sourceRootResource, Provider contentResource, + Provider permissionRootResource, + Provider diffRootResource) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; @@ -58,6 +61,7 @@ public class RepositoryResource { this.sourceRootResource = sourceRootResource; this.contentResource = contentResource; this.permissionRootResource = permissionRootResource; + this.diffRootResource = diffRootResource; } /** @@ -146,6 +150,11 @@ public class RepositoryResource { return tagRootResource.get(); } + @Path("diff/") + public DiffRootResource diff() { + return diffRootResource.get(); + } + @Path("branches/") public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) { return branchRootResource.get(); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index e134ac8f7a..f76669fbb9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -40,13 +40,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper tags) { + return new HalRepresentation(createLinks(namespace, name), embedDtos(getTagDtoList(namespace, name, tags))); + } + + public List getTagDtoList(String namespace, String name, Collection tags) { + return tags.stream().map(tag -> tagToTagDtoMapper.map(tag, new NamespaceAndName(namespace, name))).collect(toList()); + } + + private Links createLinks(String namespace, String name) { + return + linkingTo() + .self(resourceLinks.tag().all(namespace, name)) + .build(); + } + + private Embedded embedDtos(List dtos) { + return embeddedBuilder() + .with("tags", dtos) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java new file mode 100644 index 0000000000..8af036f5a3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagDto.java @@ -0,0 +1,24 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class TagDto extends HalRepresentation { + + private String name; + + private String revision; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java index 29a2a922ca..10b4607fbc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java @@ -1,20 +1,27 @@ package sonia.scm.api.v2.resources; -import javax.inject.Inject; -import javax.inject.Provider; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; public class TagRootResource { - private final Provider tagCollectionResource; - - @Inject - public TagRootResource(Provider tagCollectionResource) { - this.tagCollectionResource = tagCollectionResource; - } + @GET @Path("") - public TagCollectionResource getTagCollectionResource() { - return tagCollectionResource.get(); + public Response getAll(@DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc) { + throw new UnsupportedOperationException(); } + + @GET + @Path("{id}") + public Response get(String id) { + throw new UnsupportedOperationException(); + } + } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java new file mode 100644 index 0000000000..ada0fa2887 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java @@ -0,0 +1,31 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Tag; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class TagToTagDtoMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract TagDto map(Tag tag, @Context NamespaceAndName namespaceAndName); + + @AfterMapping + void appendLinks(@MappingTarget TagDto target, @Context NamespaceAndName namespaceAndName) { + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); + target.add(linksBuilder.build()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java index db2a9afc9f..c70bd268ff 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java @@ -1,13 +1,18 @@ package sonia.scm.api.v2.resources; +import sonia.scm.PageResult; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import javax.inject.Inject; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") -public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper { +public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ResourceLinks resourceLinks; @@ -17,18 +22,15 @@ public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper pageResult) { + return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink()); + } + + Optional createCreateLink() { + return UserPermissions.create().isPermitted() ? of(resourceLinks.userCollection().create()): empty(); } - @Override String createSelfLink() { return resourceLinks.userCollection().self(); } - - @Override - boolean isCreatePermitted() { - return UserPermissions.create().isPermitted(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index 2de00a0f58..4e4e97591a 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -39,34 +39,27 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.plugin.ExplodedSmp.PathTransformer; -//~--- JDK imports ------------------------------------------------------------ - +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; - import java.net.URL; - import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.Files; import java.nio.file.Path; - import java.text.SimpleDateFormat; - import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -370,9 +363,10 @@ public final class PluginProcessor if (Files.exists(libDir)) { - for (Path f : Files.newDirectoryStream(libDir, GLOB_JAR)) - { - urls.add(f.toUri().toURL()); + try (DirectoryStream pathDirectoryStream = Files.newDirectoryStream(libDir, GLOB_JAR)) { + for (Path f : pathDirectoryStream) { + urls.add(f.toUri().toURL()); + } } } @@ -656,7 +650,7 @@ public final class PluginProcessor break; } } - + logger.debug("move installed archive to {}", installed); Files.move(archive, installed); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 1b23b18f27..e23d3dc39b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -1,9 +1,16 @@ package sonia.scm.api.v2.resources; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -12,20 +19,35 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.repository.Branch; import sonia.scm.repository.Branches; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; import sonia.scm.repository.api.BranchesCommandBuilder; +import sonia.scm.repository.api.LogCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import java.net.URI; +import java.time.Instant; +import java.util.Date; +import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j public class BranchRootResourceTest { + public static final String BRANCH_PATH = "space/repo/branches/master"; + public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH; private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); private final URI baseUri = URI.create("/"); @@ -38,25 +60,62 @@ public class BranchRootResourceTest { @Mock private BranchesCommandBuilder branchesCommandBuilder; + @Mock + private LogCommandBuilder logCommandBuilder; + @InjectMocks private BranchToBranchDtoMapperImpl branchToDtoMapper; + private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; + + private BranchRootResource branchRootResource; + + @Mock + private BranchCollectionToDtoMapper branchCollectionToDtoMapper; + + @Mock + private ChangesetToParentDtoMapper changesetToParentDtoMapper; + + @Mock + private TagCollectionToDtoMapper tagCollectionToDtoMapper; + + + @InjectMocks + private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + @Before public void prepareEnvironment() throws Exception { + changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); - BranchRootResource branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null)), null); + branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null, null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + when(service.getBranchesCommand()).thenReturn(branchesCommandBuilder); + when(service.getLogCommand()).thenReturn(logCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); } @Test public void shouldHandleMissingBranch() throws Exception { when(branchesCommandBuilder.getBranches()).thenReturn(new Branches()); - MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/master"); + MockHttpRequest request = MockHttpRequest.get(BRANCH_URL); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -68,13 +127,40 @@ public class BranchRootResourceTest { public void shouldFindExistingBranch() throws Exception { when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(new Branch("master", "revision"))); - MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/master"); + MockHttpRequest request = MockHttpRequest.get(BRANCH_URL); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); assertEquals(200, response.getStatus()); - System.out.println(response.getContentAsString()); + log.info("Response :{}", response.getContentAsString()); assertTrue(response.getContentAsString().contains("\"revision\":\"revision\"")); } + + @Test + public void shouldFindHistory() throws Exception { + String id = "revision_123"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/"); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java new file mode 100644 index 0000000000..570830d651 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -0,0 +1,161 @@ +package sonia.scm.api.v2.resources; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.LogCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import java.net.URI; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j +public class ChangesetRootResourceTest { + + + public static final String CHANGESET_PATH = "space/repo/changesets/"; + public static final String CHANGESET_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + CHANGESET_PATH; + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private RepositoryServiceFactory serviceFactory; + + @Mock + private RepositoryService service; + + @Mock + private LogCommandBuilder logCommandBuilder; + + private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; + + @InjectMocks + private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + + private ChangesetRootResource changesetRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); + changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider + .of(new RepositoryResource(null, null, null, null, null, + MockProvider.of(changesetRootResource), null, null, null, null)), null); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + when(service.getLogCommand()).thenReturn(logCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGetChangeSets() throws Exception { + String id = "revision_123"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(CHANGESET_URL) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + + @Test + public void shouldGetChangeSet() throws Exception { + String id = "revision_123"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setEndChangeset(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(CHANGESET_URL + "id") + .accept(VndMediaType.CHANGESET); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 227775cf16..e5d6fb4296 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -136,7 +136,7 @@ public class PermissionRootResourceTest { permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource), null)), null); dispatcher = createDispatcher(repositoryRootResource); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 2029764550..539d469445 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -79,7 +79,7 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index e764e2661c..f9ace9f8f6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -19,13 +19,14 @@ public class ResourceLinksMock { when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); - when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); + when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); + when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 33607c0a6d..a9209828c3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -105,7 +105,7 @@ public class ResourceLinksTest { @Test public void shouldCreateCorrectTagCollectionUrl() { - String url = resourceLinks.tagCollection().self("space", "repo"); + String url = resourceLinks.tag().all("space", "repo"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/tags/", url); } @@ -141,7 +141,7 @@ public class ResourceLinksTest { @Test public void shouldCreateCorrectChangesetCollectionUrl() { - String url = resourceLinks.changeset().self("space", "repo"); + String url = resourceLinks.changeset().all("space", "repo"); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/changesets/", url); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 1e73c6dfaa..9bc7a27e4d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -72,10 +72,12 @@ public class SourceRootResourceTest { null, MockProvider.of(sourceRootResource), null, + null, null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); } @Test