This commit is contained in:
Mohamed Karray
2018-09-05 09:48:18 +02:00
53 changed files with 1781 additions and 1117 deletions

View File

@@ -33,11 +33,9 @@
package sonia.scm.repository; package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Objects; import com.google.common.base.Objects;
import sonia.scm.BasicPropertiesAware; import sonia.scm.BasicPropertiesAware;
import sonia.scm.Validateable; import sonia.scm.ModelObject;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil; 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.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/** /**
* Represents a changeset/commit of a repository. * Represents a changeset/commit of a repository.
@@ -59,43 +55,58 @@ import java.util.List;
*/ */
@XmlRootElement(name = "changeset") @XmlRootElement(name = "changeset")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class Changeset extends BasicPropertiesAware public class Changeset extends BasicPropertiesAware implements ModelObject {
implements Validateable, Serializable
{
/** Field description */
private static final long serialVersionUID = -8373308448928993039L; 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<String> 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<String> parents;
/**
* The tags associated with the changeset
*/
private List<String> tags;
public Changeset() {} 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) public Changeset(String id, Long date, Person author)
{ {
this(id, date, author, null); 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) public Changeset(String id, Long date, Person author, String description)
{ {
this.id = id; this.id = id;
@@ -104,16 +115,6 @@ public class Changeset extends BasicPropertiesAware
this.description = description; this.description = description;
} }
//~--- methods --------------------------------------------------------------
/**
* {@inheritDoc}
*
*
* @param obj
*
* @return
*/
@Override @Override
public boolean equals(Object obj) public boolean equals(Object obj)
{ {
@@ -122,8 +123,7 @@ public class Changeset extends BasicPropertiesAware
return false; return false;
} }
if (getClass() != obj.getClass()) if (getClass() != obj.getClass()) {
{
return false; return false;
} }
@@ -192,7 +192,21 @@ public class Changeset extends BasicPropertiesAware
return out.toString(); 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. * Returns the author of the changeset.
@@ -200,8 +214,7 @@ public class Changeset extends BasicPropertiesAware
* *
* @return author of the changeset * @return author of the changeset
*/ */
public Person getAuthor() public Person getAuthor() {
{
return author; return author;
} }
@@ -251,11 +264,27 @@ public class Changeset extends BasicPropertiesAware
* *
* @return id of the changeset * @return id of the changeset
*/ */
public String getId() @Override
{ public String getId() {
return id; 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. * Returns the file modifications, which was done with this changeset.
* *
@@ -318,8 +347,6 @@ public class Changeset extends BasicPropertiesAware
&& (date != null); && (date != null);
} }
//~--- set methods ----------------------------------------------------------
/** /**
* Sets the author of the changeset. * Sets the author of the changeset.
* *
@@ -409,30 +436,4 @@ public class Changeset extends BasicPropertiesAware
this.tags = tags; 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<String> 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<String> parents;
/** The tags associated with the changeset */
private List<String> tags;
} }

View File

@@ -249,9 +249,11 @@ public class Person implements Validateable, Serializable
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** name of the person */ /** mail address of the person */
private String mail; private String mail;
/** mail address of the person */ /**
* name of the person
*/
private String name; private String name;
} }

View File

@@ -140,7 +140,7 @@ public final class BundleCommandBuilder
throws IOException throws IOException
{ {
checkNotNull(sink, "byte sink is required"); 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)); return bundleCommand.bundle(new BundleCommandRequest(sink));
} }

View File

@@ -17,6 +17,8 @@ public class VndMediaType {
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + 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 BRANCH = PREFIX + "branch" + SUFFIX;
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;

View File

@@ -14,7 +14,9 @@ import sonia.scm.repository.client.api.RepositoryClient;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static java.lang.Thread.sleep; import static java.lang.Thread.sleep;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@@ -124,4 +126,31 @@ public class RepositoryAccessITCase {
.statusCode(HttpStatus.SC_OK) .statusCode(HttpStatus.SC_OK)
.body(equalTo("sub-a")); .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
}
} }

View File

@@ -1,10 +1,10 @@
/** /**
* Copyright (c) 2010, Sebastian Sdorra * Copyright (c) 2010, Sebastian Sdorra
* All rights reserved. * All rights reserved.
* * <p>
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
* * <p>
* 1. Redistributions of source code must retain the above copyright notice, * 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, * 2. Redistributions in binary form must reproduce the above copyright notice,
@@ -13,7 +13,7 @@
* 3. Neither the name of SCM-Manager; nor the names of its * 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this * contributors may be used to endorse or promote products derived from this
* software without specific prior written permission. * software without specific prior written permission.
* * <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,96 +24,49 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* * <p>
* http://bitbucket.org/sdorra/scm-manager * http://bitbucket.org/sdorra/scm-manager
*
*/ */
package sonia.scm.repository; package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.util.Util; import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public final class GitSubModuleParser public final class GitSubModuleParser {
{
/** private GitSubModuleParser() {
* Constructs ... }
*
*/
private GitSubModuleParser() {}
//~--- methods -------------------------------------------------------------- public static Map<String, SubRepository> parse(String content) {
Map<String, SubRepository> subRepositories = new HashMap<>();
/** try (Scanner scanner = new Scanner(content)) {
* //~--- methods --------------------------------------------------------------
*
*
* Method description
*
*
* @param content
*
* @return
*/
public static Map<String, SubRepository> parse(String content)
{
Map<String, SubRepository> subRepositories = new HashMap<String,
SubRepository>();
Scanner scanner = new Scanner(content);
SubRepository repository = null; SubRepository repository = null;
while (scanner.hasNextLine()) {
while (scanner.hasNextLine())
{
String line = scanner.nextLine(); String line = scanner.nextLine();
if (Util.isNotEmpty(line)) {
if (Util.isNotEmpty(line))
{
line = line.trim(); line = line.trim();
if (line.startsWith("[") && line.endsWith("]")) {
if (line.startsWith("[") && line.endsWith("]"))
{
repository = new SubRepository(); repository = new SubRepository();
} } else if (line.startsWith("path")) {
else if (line.startsWith("path"))
{
subRepositories.put(getValue(line), repository); subRepositories.put(getValue(line), repository);
} } else if (line.startsWith("url")) {
else if (line.startsWith("url"))
{
repository.setRepositoryUrl(getValue(line)); repository.setRepositoryUrl(getValue(line));
} }
} }
} }
}
return subRepositories; return subRepositories;
} }
//~--- get methods ---------------------------------------------------------- private static String getValue(String line) {
/**
* Method description
*
*
* @param line
*
* @return
*/
private static String getValue(String line)
{
return line.split("=")[1].trim(); return line.split("=")[1].trim();
} }
} }

View File

@@ -37,6 +37,7 @@ package sonia.scm.repository.spi;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
@@ -51,6 +52,7 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import java.io.IOException; import java.io.IOException;
@@ -156,15 +158,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
* @return * @return
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public ChangesetPagingResult getChangesets(LogCommandRequest request) public ChangesetPagingResult getChangesets(LogCommandRequest request) throws RevisionNotFoundException {
throws IOException if (logger.isDebugEnabled()) {
{
if (logger.isDebugEnabled())
{
logger.debug("fetch changesets for request: {}", request); logger.debug("fetch changesets for request: {}", request);
} }
@@ -172,17 +170,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
GitChangesetConverter converter = null; GitChangesetConverter converter = null;
RevWalk revWalk = null; RevWalk revWalk = null;
try (org.eclipse.jgit.lib.Repository gr = open()) try (org.eclipse.jgit.lib.Repository gr = open()) {
{ if (!gr.getAllRefs().isEmpty()) {
if (!gr.getAllRefs().isEmpty())
{
int counter = 0; int counter = 0;
int start = request.getPagingStart(); int start = request.getPagingStart();
if (start < 0) if (start < 0) {
{ if (logger.isErrorEnabled()) {
if (logger.isErrorEnabled())
{
logger.error("start parameter is negative, reset to 0"); logger.error("start parameter is negative, reset to 0");
} }
@@ -193,15 +187,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
int limit = request.getPagingLimit(); int limit = request.getPagingLimit();
ObjectId startId = null; ObjectId startId = null;
if (!Strings.isNullOrEmpty(request.getStartChangeset())) if (!Strings.isNullOrEmpty(request.getStartChangeset())) {
{
startId = gr.resolve(request.getStartChangeset()); startId = gr.resolve(request.getStartChangeset());
} }
ObjectId endId = null; ObjectId endId = null;
if (!Strings.isNullOrEmpty(request.getEndChangeset())) if (!Strings.isNullOrEmpty(request.getEndChangeset())) {
{
endId = gr.resolve(request.getEndChangeset()); endId = gr.resolve(request.getEndChangeset());
} }
@@ -209,8 +201,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
converter = new GitChangesetConverter(gr, revWalk); converter = new GitChangesetConverter(gr, revWalk);
if (!Strings.isNullOrEmpty(request.getPath())) if (!Strings.isNullOrEmpty(request.getPath())) {
{
revWalk.setTreeFilter( revWalk.setTreeFilter(
AndTreeFilter.create( AndTreeFilter.create(
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF)); PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
@@ -218,48 +209,43 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
ObjectId head = getBranchOrDefault(gr, request.getBranch()); ObjectId head = getBranchOrDefault(gr, request.getBranch());
if (head != null) if (head != null) {
{ if (startId != null) {
if (startId != null)
{
revWalk.markStart(revWalk.lookupCommit(startId)); revWalk.markStart(revWalk.lookupCommit(startId));
} } else {
else
{
revWalk.markStart(revWalk.lookupCommit(head)); revWalk.markStart(revWalk.lookupCommit(head));
} }
Iterator<RevCommit> iterator = revWalk.iterator(); Iterator<RevCommit> iterator = revWalk.iterator();
while (iterator.hasNext()) while (iterator.hasNext()) {
{
RevCommit commit = iterator.next(); RevCommit commit = iterator.next();
if ((counter >= start) if ((counter >= start)
&& ((limit < 0) || (counter < start + limit))) && ((limit < 0) || (counter < start + limit))) {
{
changesetList.add(converter.createChangeset(commit)); changesetList.add(converter.createChangeset(commit));
} }
counter++; counter++;
if ((endId != null) && commit.getId().equals(endId)) if ((endId != null) && commit.getId().equals(endId)) {
{
break; break;
} }
} }
} }
changesets = new ChangesetPagingResult(counter, changesetList); changesets = new ChangesetPagingResult(counter, changesetList);
} } else if (logger.isWarnEnabled()) {
else if (logger.isWarnEnabled())
{
logger.warn("the repository {} seems to be empty", logger.warn("the repository {} seems to be empty",
repository.getName()); repository.getName());
changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST); changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST);
} }
} }
catch (MissingObjectException e)
{
throw new RevisionNotFoundException(e.getObjectId().name());
}
catch (Exception ex) catch (Exception ex)
{ {
throw new InternalRepositoryException("could not create change log", ex); throw new InternalRepositoryException("could not create change log", ex);

View File

@@ -35,15 +35,12 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.Test; import org.junit.Test;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitConstants; import sonia.scm.repository.GitConstants;
import sonia.scm.repository.Modifications; import sonia.scm.repository.Modifications;
import java.io.IOException;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -63,13 +60,9 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
/** /**
* Tests log command with the usage of a default branch. * Tests log command with the usage of a default branch.
*
* @throws IOException
* @throws GitAPIException
* @
*/ */
@Test @Test
public void testGetDefaultBranch() throws IOException, GitAPIException { public void testGetDefaultBranch() throws Exception {
// without default branch, the repository head should be used // without default branch, the repository head should be used
ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest()); ChangesetPagingResult result = createCommand().getChangesets(new LogCommandRequest());
@@ -92,15 +85,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId());
} }
/**
* Method description
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test @Test
public void testGetAll() throws IOException public void testGetAll() throws Exception
{ {
ChangesetPagingResult result = ChangesetPagingResult result =
createCommand().getChangesets(new LogCommandRequest()); createCommand().getChangesets(new LogCommandRequest());
@@ -110,15 +96,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals(4, result.getChangesets().size()); assertEquals(4, result.getChangesets().size());
} }
/**
* Method description
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test @Test
public void testGetAllByPath() throws IOException public void testGetAllByPath() throws Exception
{ {
LogCommandRequest request = new LogCommandRequest(); LogCommandRequest request = new LogCommandRequest();
@@ -133,15 +112,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(1).getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(1).getId());
} }
/**
* Method description
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test @Test
public void testGetAllWithLimit() throws IOException public void testGetAllWithLimit() throws Exception
{ {
LogCommandRequest request = new LogCommandRequest(); LogCommandRequest request = new LogCommandRequest();
@@ -164,15 +136,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", c2.getId()); assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", c2.getId());
} }
/**
* Method description
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test @Test
public void testGetAllWithPaging() throws IOException public void testGetAllWithPaging() throws Exception
{ {
LogCommandRequest request = new LogCommandRequest(); LogCommandRequest request = new LogCommandRequest();
@@ -196,10 +161,6 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", c2.getId()); assertEquals("592d797cd36432e591416e8b2b98154f4f163411", c2.getId());
} }
/**
* Method description
*
*/
@Test @Test
public void testGetCommit() public void testGetCommit()
{ {
@@ -224,15 +185,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertThat(mods.getAdded(), contains("a.txt", "b.txt")); assertThat(mods.getAdded(), contains("a.txt", "b.txt"));
} }
/**
* Method description
*
*
* @throws IOException
* @throws RepositoryException
*/
@Test @Test
public void testGetRange() throws IOException public void testGetRange() throws Exception
{ {
LogCommandRequest request = new LogCommandRequest(); LogCommandRequest request = new LogCommandRequest();
@@ -254,12 +208,6 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId()); assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
} }
/**
* Method description
*
*
* @return
*/
private GitLogCommand createCommand() private GitLogCommand createCommand()
{ {
return new GitLogCommand(createContext(), repository); return new GitLogCommand(createContext(), repository);

View File

@@ -1,29 +1,3 @@
{ {
"parser": "babel-eslint", "extends": "@scm-manager/eslint-config"
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:flowtype/recommended"
],
"plugins": [
"flowtype",
"react",
"jsx-a11y",
"import"
],
"rules": {
"quotes": ["error", "double"]
},
"env": {
"browser": true
},
"overrides": [
{
"files": [ "*.test.js" ],
"env": {
"jest": true,
"browser": true
}
}
]
} }

View File

@@ -42,18 +42,11 @@
"pre-commit": "jest && flow && eslint src" "pre-commit": "jest && flow && eslint src"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.7", "@scm-manager/ui-bundler": "^0.0.9",
"babel-eslint": "^8.2.6",
"copyfiles": "^2.0.0", "copyfiles": "^2.0.0",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1", "enzyme-adapter-react-16": "^1.1.1",
"eslint": "^5.3.0",
"eslint-plugin-flowtype": "^2.50.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.10.0",
"fetch-mock": "^6.5.0", "fetch-mock": "^6.5.0",
"flow-bin": "^0.77.0",
"flow-typed": "^2.5.1", "flow-typed": "^2.5.1",
"jest": "^23.5.0", "jest": "^23.5.0",
"node-sass-chokidar": "^1.3.0", "node-sass-chokidar": "^1.3.0",

View File

@@ -74,6 +74,7 @@ class PluginLoader extends React.Component<Props, State> {
}) })
.then(script => { .then(script => {
// TODO is this safe??? // TODO is this safe???
// eslint-disable-next-line no-eval
eval(script); // NOSONAR eval(script); // NOSONAR
}); });
}; };

View File

@@ -1,3 +1,4 @@
// @flow
import React from "react"; import React from "react";
type Props = { type Props = {

View File

@@ -1,3 +1,4 @@
// @flow
import React from "react"; import React from "react";
type Props = { type Props = {

View File

@@ -1,18 +1,26 @@
// @flow
//modified from https://github.com/GA-MO/react-confirm-alert //modified from https://github.com/GA-MO/react-confirm-alert
import React from "react"; import * as React from "react";
import { render, unmountComponentAtNode } from "react-dom"; import { render, unmountComponentAtNode } from "react-dom";
import "./ConfirmAlert.css"; import "./ConfirmAlert.css";
type Button = {
label: string,
onClick: () => void | null
};
type Props = { type Props = {
title: string, title: string,
message: string, message: string,
buttons: array buttons: Button[]
}; };
class ConfirmAlert extends React.Component<Props> { class ConfirmAlert extends React.Component<Props> {
handleClickButton = button => { handleClickButton = (button: Button) => {
if (button.onClick) button.onClick(); if (button.onClick) {
button.onClick();
}
this.close(); this.close();
}; };
@@ -48,20 +56,26 @@ class ConfirmAlert extends React.Component<Props> {
} }
} }
function createElementReconfirm(properties) { function createElementReconfirm(properties: Props) {
const divTarget = document.createElement("div"); const divTarget = document.createElement("div");
divTarget.id = "react-confirm-alert"; divTarget.id = "react-confirm-alert";
if (document.body) {
document.body.appendChild(divTarget); document.body.appendChild(divTarget);
render(<ConfirmAlert {...properties} />, divTarget); render(<ConfirmAlert {...properties} />, divTarget);
}
} }
function removeElementReconfirm() { function removeElementReconfirm() {
const target = document.getElementById("react-confirm-alert"); const target = document.getElementById("react-confirm-alert");
if (target) {
unmountComponentAtNode(target); unmountComponentAtNode(target);
if (target.parentNode) {
target.parentNode.removeChild(target); target.parentNode.removeChild(target);
}
}
} }
export function confirmAlert(properties) { export function confirmAlert(properties: Props) {
createElementReconfirm(properties); createElementReconfirm(properties);
} }

View File

@@ -1,3 +1,4 @@
// @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Route } from "react-router"; import { Route } from "react-router";

View File

@@ -1,3 +1,4 @@
// @flow
import React, { Component } from "react"; import React, { Component } from "react";
import Main from "./Main"; import Main from "./Main";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -15,6 +16,7 @@ import { PrimaryNavigation } from "../components/navigation";
import Loading from "../components/Loading"; import Loading from "../components/Loading";
import ErrorPage from "../components/ErrorPage"; import ErrorPage from "../components/ErrorPage";
import { Footer, Header } from "../components/layout"; import { Footer, Header } from "../components/layout";
import type { Me } from "../types/Me";
type Props = { type Props = {
me: Me, me: Me,

View File

@@ -1,5 +1,5 @@
//@flow //@flow
import React from 'react'; import React from "react";
import NavLink from "../../../components/navigation/NavLink"; import NavLink from "../../../components/navigation/NavLink";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import type { Group } from "../../types/Group"; import type { Group } from "../../types/Group";

View File

@@ -217,7 +217,7 @@ describe("groups fetch()", () => {
const callMe = () => { const callMe = () => {
called = true; called = true;
} };
const store = mockStore({}); const store = mockStore({});
return store.dispatch(createGroup(humanGroup, callMe)).then(() => { return store.dispatch(createGroup(humanGroup, callMe)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
@@ -254,9 +254,9 @@ describe("groups fetch()", () => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING); expect(actions[0].type).toEqual(MODIFY_GROUP_PENDING);
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS); expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
expect(actions[1].payload).toEqual(humanGroup) expect(actions[1].payload).toEqual(humanGroup);
});
}); });
})
it("should call the callback after modifying group", () => { it("should call the callback after modifying group", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", { fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
@@ -266,7 +266,7 @@ describe("groups fetch()", () => {
let called = false; let called = false;
const callback = () => { const callback = () => {
called = true; called = true;
} };
const store = mockStore({}); const store = mockStore({});
return store.dispatch(modifyGroup(humanGroup, callback)).then(() => { return store.dispatch(modifyGroup(humanGroup, callback)).then(() => {
@@ -275,7 +275,7 @@ describe("groups fetch()", () => {
expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS); expect(actions[1].type).toEqual(MODIFY_GROUP_SUCCESS);
expect(called).toBe(true); expect(called).toBe(true);
}); });
}) });
it("should fail modifying group on HTTP 500", () => { it("should fail modifying group on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", { fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
@@ -290,7 +290,7 @@ describe("groups fetch()", () => {
expect(actions[1].type).toEqual(MODIFY_GROUP_FAILURE); expect(actions[1].type).toEqual(MODIFY_GROUP_FAILURE);
expect(actions[1].payload).toBeDefined(); expect(actions[1].payload).toBeDefined();
}); });
}) });
it("should delete successfully group humanGroup", () => { it("should delete successfully group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", { fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
@@ -493,7 +493,7 @@ describe("selector tests", () => {
}); });
it("should return null when there are no groups in the state", () => { it("should return null when there are no groups in the state", () => {
expect(getGroupsFromState({})).toBe(null) expect(getGroupsFromState({})).toBe(null);
}); });
it("should return true, when fetch groups is pending", () => { it("should return true, when fetch groups is pending", () => {
@@ -563,23 +563,23 @@ describe("selector tests", () => {
expect(isCreateGroupPending({pending: { expect(isCreateGroupPending({pending: {
[CREATE_GROUP]: true [CREATE_GROUP]: true
}})).toBeTruthy(); }})).toBeTruthy();
}) });
it("should return false if create group is not pending", () => { it("should return false if create group is not pending", () => {
expect(isCreateGroupPending({})).toBe(false); expect(isCreateGroupPending({})).toBe(false);
}) });
it("should return error if creating group failed", () => { it("should return error if creating group failed", () => {
expect(getCreateGroupFailure({ expect(getCreateGroupFailure({
failure: { failure: {
[CREATE_GROUP]: error [CREATE_GROUP]: error
} }
})).toEqual(error) })).toEqual(error);
}) });
it("should return undefined if creating group did not fail", () => { it("should return undefined if creating group did not fail", () => {
expect(getCreateGroupFailure({})).toBeUndefined() expect(getCreateGroupFailure({})).toBeUndefined();
}) });
it("should return true, when delete group humanGroup is pending", () => { it("should return true, when delete group humanGroup is pending", () => {
@@ -613,20 +613,20 @@ describe("selector tests", () => {
pending: { pending: {
[CREATE_GROUP]: true [CREATE_GROUP]: true
} }
} };
expect(isCreateGroupPending(state)).toBe(true); expect(isCreateGroupPending(state)).toBe(true);
}) });
it("should return false, if createGroup is not pending", () => { it("should return false, if createGroup is not pending", () => {
expect(isCreateGroupPending({})).toBe(false) expect(isCreateGroupPending({})).toBe(false);
}) });
it("should return error of createGroup failed", () => { it("should return error of createGroup failed", () => {
const state = { const state = {
failure: { failure: {
[CREATE_GROUP]: error [CREATE_GROUP]: error
} }
} };
expect(getCreateGroupFailure(state)).toEqual(error) expect(getCreateGroupFailure(state)).toEqual(error);
}) });
}); });

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
// In production, we register a service worker to serve assets from local cache. // In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives // This lets the app load faster on subsequent visits in production, and gives
@@ -9,9 +10,9 @@
// This link also includes instructions on opting out of this behavior. // This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean( const isLocalhost = Boolean(
window.location.hostname === 'localhost' || window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
@@ -19,7 +20,7 @@ const isLocalhost = Boolean(
); );
export default function register() { export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location); const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
@@ -29,7 +30,7 @@ export default function register() {
return; return;
} }
window.addEventListener('load', () => { window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) { if (isLocalhost) {
@@ -40,8 +41,8 @@ export default function register() {
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + "This web app is being served cache-first by a service " +
'worker. To learn more, visit https://goo.gl/SC7cgQ' "worker. To learn more, visit https://goo.gl/SC7cgQ"
); );
}); });
} else { } else {
@@ -59,25 +60,25 @@ function registerValidSW(swUrl) {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing;
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and // At this point, the old content will have been purged and
// the fresh content will have been added to the cache. // the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is // It's the perfect time to display a "New content is
// available; please refresh." message in your web app. // available; please refresh." message in your web app.
console.log('New content is available; please refresh.'); console.log("New content is available; please refresh.");
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log("Content is cached for offline use.");
} }
} }
}; };
}; };
}) })
.catch(error => { .catch(error => {
console.error('Error during service worker registration:', error); console.error("Error during service worker registration:", error);
}); });
} }
@@ -88,7 +89,7 @@ function checkValidServiceWorker(swUrl) {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
if ( if (
response.status === 404 || response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1 response.headers.get("content-type").indexOf("javascript") === -1
) { ) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then(registration => {
@@ -103,15 +104,17 @@ function checkValidServiceWorker(swUrl) {
}) })
.catch(() => { .catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.' "No internet connection found. App is running in offline mode."
); );
}); });
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then(registration => {
registration.unregister(); registration.unregister();
}); });
} }
} }
/* eslint-enable no-console */

View File

@@ -6,7 +6,6 @@ import type { Repository } from "../../types/Repositories";
import DateFromNow from "../../../components/DateFromNow"; import DateFromNow from "../../../components/DateFromNow";
import RepositoryEntryLink from "./RepositoryEntryLink"; import RepositoryEntryLink from "./RepositoryEntryLink";
import classNames from "classnames"; import classNames from "classnames";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import RepositoryAvatar from "./RepositoryAvatar"; import RepositoryAvatar from "./RepositoryAvatar";
const styles = { const styles = {

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ import sonia.scm.PageResult;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import static com.damnhandy.uri.template.UriTemplate.fromTemplate; import static com.damnhandy.uri.template.UriTemplate.fromTemplate;
import static de.otto.edison.hal.Embedded.embeddedBuilder; 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 de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation> { abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation, M extends BaseMapper<E, D>> {
private final String collectionName; private final String collectionName;
private final BaseMapper<E, D> entityToDtoMapper;
private final M entityToDtoMapper;
@Inject @Inject
public BasicCollectionToDtoMapper(String collectionName, BaseMapper<E, D> entityToDtoMapper) { public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) {
this.collectionName = collectionName; this.collectionName = collectionName;
this.entityToDtoMapper = entityToDtoMapper; this.entityToDtoMapper = entityToDtoMapper;
} }
public CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult) { CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink) {
NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount()); return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map);
List<D> dtos = pageResult.getEntities().stream().map(entityToDtoMapper::map).collect(toList()); }
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink, Function<E, ? extends HalRepresentation> mapper) {
NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount());
List<HalRepresentation> dtos = pageResult.getEntities().stream().map(mapper).collect(toList());
CollectionDto collectionDto = new CollectionDto( CollectionDto collectionDto = new CollectionDto(
createLinks(paging), createLinks(paging, selfLink, createLink),
embedDtos(dtos) embedDtos(dtos));
);
collectionDto.setPage(pageNumber); collectionDto.setPage(pageNumber);
collectionDto.setPageTotal(computePageTotal(pageSize, pageResult)); collectionDto.setPageTotal(computePageTotal(pageSize, pageResult));
return collectionDto; return collectionDto;
@@ -51,26 +56,16 @@ abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRe
} }
} }
private Links createLinks(NumberedPaging page) { private Links createLinks(NumberedPaging page, String selfLink, Optional<String> createLink) {
String baseUrl = createSelfLink();
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.with(page.links( .with(page.links(
fromTemplate(baseUrl + "{?page,pageSize}"), fromTemplate(selfLink + "{?page,pageSize}"),
EnumSet.allOf(PagingRel.class))); EnumSet.allOf(PagingRel.class)));
if (isCreatePermitted()) { createLink.ifPresent(link -> linksBuilder.single(link("create", link)));
linksBuilder.single(link("create", createCreateLink()));
}
return linksBuilder.build(); return linksBuilder.build();
} }
abstract boolean isCreatePermitted(); private Embedded embedDtos(List<HalRepresentation> dtos) {
abstract String createCreateLink();
abstract String createSelfLink();
private Embedded embedDtos(List<D> dtos) {
return embeddedBuilder() return embeddedBuilder()
.with(collectionName, dtos) .with(collectionName, dtos)
.build(); .build();

View File

@@ -26,8 +26,11 @@ public class BranchCollectionToDtoMapper {
} }
public HalRepresentation map(String namespace, String name, Collection<Branch> branches) { public HalRepresentation map(String namespace, String name, Collection<Branch> branches) {
List<BranchDto> dtos = branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches)));
return new HalRepresentation(createLinks(namespace, name), embedDtos(dtos)); }
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) {
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
} }
private Links createLinks(String namespace, String name) { private Links createLinks(String namespace, String name) {

View File

@@ -3,33 +3,43 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.PageResult;
import sonia.scm.repository.Branches; import sonia.scm.repository.Branches;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryNotFoundException; import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.CommandNotSupportedException; import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
public class BranchRootResource { public class BranchRootResource {
private final RepositoryServiceFactory servicefactory; private final RepositoryServiceFactory serviceFactory;
private final BranchToBranchDtoMapper branchToDtoMapper; private final BranchToBranchDtoMapper branchToDtoMapper;
private final BranchCollectionToDtoMapper branchCollectionToDtoMapper; private final BranchCollectionToDtoMapper branchCollectionToDtoMapper;
private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper;
@Inject @Inject
public BranchRootResource(RepositoryServiceFactory servicefactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper) { public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
this.servicefactory = servicefactory; this.serviceFactory = serviceFactory;
this.branchToDtoMapper = branchToDtoMapper; this.branchToDtoMapper = branchToDtoMapper;
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper; this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
} }
/** /**
@@ -40,7 +50,6 @@ public class BranchRootResource {
* @param namespace the namespace of the repository * @param namespace the namespace of the repository
* @param name the name of the repository * @param name the name of the repository
* @param branchName the name of the branch * @param branchName the name of the branch
*
*/ */
@GET @GET
@Path("{branch}") @Path("{branch}")
@@ -55,7 +64,7 @@ public class BranchRootResource {
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException { 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(); Branches branches = repositoryService.getBranchesCommand().getBranches();
return branches.getBranches() return branches.getBranches()
.stream() .stream()
@@ -74,8 +83,35 @@ public class BranchRootResource {
@Path("{branch}/changesets/") @Path("{branch}/changesets/")
@GET @GET
public Response history(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) { @StatusCodes({
throw new UnsupportedOperationException(); @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<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
} else {
return Response.ok().build();
}
}
} }
/** /**
@@ -85,7 +121,6 @@ public class BranchRootResource {
* *
* @param namespace the namespace of the repository * @param namespace the namespace of the repository
* @param name the name of the repository * @param name the name of the repository
*
*/ */
@GET @GET
@Path("") @Path("")
@@ -100,7 +135,7 @@ public class BranchRootResource {
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { 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(); Branches branches = repositoryService.getBranchesCommand().getBranches();
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build(); return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
} catch (CommandNotSupportedException ex) { } catch (CommandNotSupportedException ex) {

View File

@@ -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<Changeset, ChangesetDto, ChangesetToChangesetDtoMapper> {
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<Changeset> 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());
}
}

View File

@@ -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<? extends HalRepresentation> halRepresentations) {
return super.withEmbedded(rel, halRepresentations);
}
}

View File

@@ -1,25 +1,101 @@
package sonia.scm.api.v2.resources; 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.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException;
@Slf4j
public class ChangesetRootResource { 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 @GET
@Path("") @Path("")
public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @StatusCodes({
@DefaultValue("10") @QueryParam("pageSize") int pageSize, @ResponseCode(code = 200, condition = "success"),
@QueryParam("sortBy") String sortBy, @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@DefaultValue("false") @QueryParam("desc") boolean desc) { @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"),
throw new UnsupportedOperationException(); @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<Changeset> pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
} else {
return Response.ok().build();
}
}
} }
@GET @GET
@Path("{revision}") @StatusCodes({
public Response get() { @ResponseCode(code = 200, condition = "success"),
throw new UnsupportedOperationException(); @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();
}
}
} }
} }

View File

@@ -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<Changeset, ChangesetDto> {
@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 <T> List<T> getListOfObjects(List<String> list, Function<String, T> mapFunction) {
return list
.stream()
.map(mapFunction)
.collect(Collectors.toList());
}
}

View File

@@ -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<Changeset, ParentChangesetDto> {
@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());
}
}

View File

@@ -1,32 +0,0 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Links.linkingTo;
abstract class CollectionToDtoMapper<E, D extends HalRepresentation> {
private final String collectionName;
private final BaseMapper<E, D> mapper;
protected CollectionToDtoMapper(String collectionName, BaseMapper<E, D> mapper) {
this.collectionName = collectionName;
this.mapper = mapper;
}
public HalRepresentation map(Collection<E> collection) {
List<D> dtos = collection.stream().map(mapper::map).collect(Collectors.toList());
return new HalRepresentation(
linkingTo().self(createSelfLink()).build(),
embeddedBuilder().with(collectionName, dtos).build()
);
}
protected abstract String createSelfLink();
}

View File

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

View File

@@ -1,11 +1,16 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupPermissions; import sonia.scm.group.GroupPermissions;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.Optional;
public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper<Group, GroupDto> { import static java.util.Optional.empty;
import static java.util.Optional.of;
public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper<Group, GroupDto, GroupToGroupDtoMapper> {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
@@ -15,18 +20,15 @@ public class GroupCollectionToDtoMapper extends BasicCollectionToDtoMapper<Group
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }
@Override public CollectionDto map(int pageNumber, int pageSize, PageResult<Group> pageResult) {
String createCreateLink() { return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
return resourceLinks.groupCollection().create();
} }
@Override private Optional<String> createCreateLink() {
String createSelfLink() { return GroupPermissions.create().isPermitted() ? of(resourceLinks.groupCollection().create()): empty();
}
private String createSelfLink() {
return resourceLinks.groupCollection().self(); return resourceLinks.groupCollection().self();
} }
@Override
boolean isCreatePermitted() {
return GroupPermissions.create().isPermitted();
}
} }

View File

@@ -28,6 +28,11 @@ public class MapperModule extends AbstractModule {
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass());
bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.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()); bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass());
// no mapstruct required // no mapstruct required

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import javax.inject.Inject; 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. // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306") @SuppressWarnings("squid:S3306")
public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<Repository, RepositoryDto> { public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<Repository, RepositoryDto, RepositoryToRepositoryDtoMapper> {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
@@ -17,18 +22,15 @@ public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }
@Override public CollectionDto map(int pageNumber, int pageSize, PageResult<Repository> pageResult) {
String createCreateLink() { return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
return resourceLinks.repositoryCollection().create(); }
Optional<String> createCreateLink() {
return RepositoryPermissions.create().isPermitted() ? of(resourceLinks.repositoryCollection().create()): empty();
} }
@Override
String createSelfLink() { String createSelfLink() {
return resourceLinks.repositoryCollection().self(); return resourceLinks.repositoryCollection().self();
} }
@Override
boolean isCreatePermitted() {
return RepositoryPermissions.create().isPermitted();
}
} }

View File

@@ -39,6 +39,7 @@ public class RepositoryResource {
private final Provider<SourceRootResource> sourceRootResource; private final Provider<SourceRootResource> sourceRootResource;
private final Provider<ContentResource> contentResource; private final Provider<ContentResource> contentResource;
private final Provider<PermissionRootResource> permissionRootResource; private final Provider<PermissionRootResource> permissionRootResource;
private final Provider<DiffRootResource> diffRootResource;
@Inject @Inject
public RepositoryResource( public RepositoryResource(
@@ -47,7 +48,9 @@ public class RepositoryResource {
Provider<TagRootResource> tagRootResource, Provider<TagRootResource> tagRootResource,
Provider<BranchRootResource> branchRootResource, Provider<BranchRootResource> branchRootResource,
Provider<ChangesetRootResource> changesetRootResource, Provider<ChangesetRootResource> changesetRootResource,
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource, Provider<PermissionRootResource> permissionRootResource) { Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
Provider<PermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager; this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper; this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -58,6 +61,7 @@ public class RepositoryResource {
this.sourceRootResource = sourceRootResource; this.sourceRootResource = sourceRootResource;
this.contentResource = contentResource; this.contentResource = contentResource;
this.permissionRootResource = permissionRootResource; this.permissionRootResource = permissionRootResource;
this.diffRootResource = diffRootResource;
} }
/** /**
@@ -146,6 +150,11 @@ public class RepositoryResource {
return tagRootResource.get(); return tagRootResource.get();
} }
@Path("diff/")
public DiffRootResource diff() {
return diffRootResource.get();
}
@Path("branches/") @Path("branches/")
public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) { public BranchRootResource branches(@PathParam("namespace") String namespace, @PathParam("name") String name) {
return branchRootResource.get(); return branchRootResource.get();

View File

@@ -40,13 +40,13 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
} }
try (RepositoryService repositoryService = serviceFactory.create(repository)) { try (RepositoryService repositoryService = serviceFactory.create(repository)) {
if (repositoryService.isSupported(Command.TAGS)) { if (repositoryService.isSupported(Command.TAGS)) {
linksBuilder.single(link("tags", resourceLinks.tagCollection().self(target.getNamespace(), target.getName()))); linksBuilder.single(link("tags", resourceLinks.tag().all(target.getNamespace(), target.getName())));
} }
if (repositoryService.isSupported(Command.BRANCHES)) { if (repositoryService.isSupported(Command.BRANCHES)) {
linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName()))); linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName())));
} }
} }
linksBuilder.single(link("changesets", resourceLinks.changeset().self(target.getNamespace(), target.getName()))); linksBuilder.single(link("changesets", resourceLinks.changeset().all(target.getNamespace(), target.getName())));
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName()))); linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName())));
target.add(linksBuilder.build()); target.add(linksBuilder.build());
} }

View File

@@ -207,7 +207,7 @@ class ResourceLinks {
} }
public TagCollectionLinks tagCollection() { public TagCollectionLinks tag() {
return new TagCollectionLinks(uriInfoStore.get()); return new TagCollectionLinks(uriInfoStore.get());
} }
@@ -215,11 +215,35 @@ class ResourceLinks {
private final LinkBuilder tagLinkBuilder; private final LinkBuilder tagLinkBuilder;
TagCollectionLinks(UriInfo uriInfo) { TagCollectionLinks(UriInfo uriInfo) {
tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class, TagCollectionResource.class); tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class);
} }
String self(String namespace, String name) { String self(String namespace, String name, String id) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getTagCollectionResource").parameters().method("getAll").parameters().href(); return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(id).href();
}
String all(String namespace, String name) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
}
}
public DiffLinks diff() {
return new DiffLinks(uriInfoStore.get());
}
static class DiffLinks {
private final LinkBuilder diffLinkBuilder;
DiffLinks(UriInfo uriInfo) {
diffLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, DiffRootResource.class);
}
String self(String namespace, String name, String id) {
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("get").parameters(id).href();
}
String all(String namespace, String name) {
return diffLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("diff").parameters().method("getAll").parameters().href();
} }
} }
@@ -270,7 +294,11 @@ class ResourceLinks {
changesetLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, ChangesetRootResource.class); changesetLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, ChangesetRootResource.class);
} }
String self(String namespace, String name) { String self(String namespace, String name, String changesetId) {
return changesetLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("changesets").parameters().method("get").parameters(changesetId).href();
}
String all(String namespace, String name) {
return changesetLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("changesets").parameters().method("getAll").parameters().href(); return changesetLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("changesets").parameters().method("getAll").parameters().href();
} }

View File

@@ -1,18 +0,0 @@
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 TagCollectionResource {
@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();
}
}

View File

@@ -0,0 +1,49 @@
package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Tag;
import java.util.Collection;
import java.util.List;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Links.linkingTo;
import static java.util.stream.Collectors.toList;
public class TagCollectionToDtoMapper {
private final ResourceLinks resourceLinks;
private final TagToTagDtoMapper tagToTagDtoMapper;
@Inject
public TagCollectionToDtoMapper(ResourceLinks resourceLinks, TagToTagDtoMapper tagToTagDtoMapper) {
this.resourceLinks = resourceLinks;
this.tagToTagDtoMapper = tagToTagDtoMapper;
}
public HalRepresentation map(String namespace, String name, Collection<Tag> tags) {
return new HalRepresentation(createLinks(namespace, name), embedDtos(getTagDtoList(namespace, name, tags)));
}
public List<TagDto> getTagDtoList(String namespace, String name, Collection<Tag> 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<TagDto> dtos) {
return embeddedBuilder()
.with("tags", dtos)
.build();
}
}

View File

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

View File

@@ -1,20 +1,27 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import javax.inject.Inject; import javax.ws.rs.DefaultValue;
import javax.inject.Provider; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
public class TagRootResource { public class TagRootResource {
private final Provider<TagCollectionResource> tagCollectionResource;
@Inject
public TagRootResource(Provider<TagCollectionResource> tagCollectionResource) {
this.tagCollectionResource = tagCollectionResource;
}
@GET
@Path("") @Path("")
public TagCollectionResource getTagCollectionResource() { public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
return tagCollectionResource.get(); @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();
}
} }

View File

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

View File

@@ -1,13 +1,18 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import sonia.scm.PageResult;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserPermissions; import sonia.scm.user.UserPermissions;
import javax.inject.Inject; 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. // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306") @SuppressWarnings("squid:S3306")
public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto> { public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto, UserToUserDtoMapper> {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
@@ -17,18 +22,15 @@ public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User,
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }
@Override public CollectionDto map(int pageNumber, int pageSize, PageResult<User> pageResult) {
String createCreateLink() { return map(pageNumber, pageSize, pageResult, this.createSelfLink(), this.createCreateLink());
return resourceLinks.userCollection().create(); }
Optional<String> createCreateLink() {
return UserPermissions.create().isPermitted() ? of(resourceLinks.userCollection().create()): empty();
} }
@Override
String createSelfLink() { String createSelfLink() {
return resourceLinks.userCollection().self(); return resourceLinks.userCollection().self();
} }
@Override
boolean isCreatePermitted() {
return UserPermissions.create().isPermitted();
}
} }

View File

@@ -39,34 +39,27 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.plugin.ExplodedSmp.PathTransformer; 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.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter; import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.xml.bind.JAXBContext; //~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.JAXBException;
/** /**
* *
@@ -370,11 +363,12 @@ public final class PluginProcessor
if (Files.exists(libDir)) if (Files.exists(libDir))
{ {
for (Path f : Files.newDirectoryStream(libDir, GLOB_JAR)) try (DirectoryStream<Path> pathDirectoryStream = Files.newDirectoryStream(libDir, GLOB_JAR)) {
{ for (Path f : pathDirectoryStream) {
urls.add(f.toUri().toURL()); urls.add(f.toUri().toURL());
} }
} }
}
ClassLoader classLoader; ClassLoader classLoader;
URL[] urlArray = urls.toArray(new URL[urls.size()]); URL[] urlArray = urls.toArray(new URL[urls.size()]);

View File

@@ -1,9 +1,16 @@
package sonia.scm.api.v2.resources; 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.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -12,20 +19,35 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.Branches; import sonia.scm.repository.Branches;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName; 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.BranchesCommandBuilder;
import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import java.net.URI; 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.assertEquals;
import static org.junit.Assert.assertTrue; 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; import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j
public class BranchRootResourceTest { 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 Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
@@ -38,25 +60,62 @@ public class BranchRootResourceTest {
@Mock @Mock
private BranchesCommandBuilder branchesCommandBuilder; private BranchesCommandBuilder branchesCommandBuilder;
@Mock
private LogCommandBuilder logCommandBuilder;
@InjectMocks @InjectMocks
private BranchToBranchDtoMapperImpl branchToDtoMapper; 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 @Before
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
BranchRootResource branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper); 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); 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); dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); 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.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 @Test
public void shouldHandleMissingBranch() throws Exception { public void shouldHandleMissingBranch() throws Exception {
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches()); 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(); MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -68,13 +127,40 @@ public class BranchRootResourceTest {
public void shouldFindExistingBranch() throws Exception { public void shouldFindExistingBranch() throws Exception {
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(new Branch("master", "revision"))); 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(); MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
assertEquals(200, response.getStatus()); assertEquals(200, response.getStatus());
System.out.println(response.getContentAsString()); log.info("Response :{}", response.getContentAsString());
assertTrue(response.getContentAsString().contains("\"revision\":\"revision\"")); 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<Changeset> 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)));
}
} }

View File

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

View File

@@ -138,7 +138,7 @@ public class PermissionRootResourceTest {
permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider 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); dispatcher = createDispatcher(repositoryRootResource);
subjectThreadState.bind(); subjectThreadState.bind();
ThreadContext.bind(subject); ThreadContext.bind(subject);

View File

@@ -79,7 +79,7 @@ public class RepositoryRootResourceTest {
@Before @Before
public void prepareEnvironment() { public void prepareEnvironment() {
initMocks(this); 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); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource));

View File

@@ -19,13 +19,14 @@ public class ResourceLinksMock {
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo));
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(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.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo));
when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo));
when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo));
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(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.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo));

View File

@@ -105,7 +105,7 @@ public class ResourceLinksTest {
@Test @Test
public void shouldCreateCorrectTagCollectionUrl() { 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); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/tags/", url);
} }
@@ -141,7 +141,7 @@ public class ResourceLinksTest {
@Test @Test
public void shouldCreateCorrectChangesetCollectionUrl() { 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); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/changesets/", url);
} }

View File

@@ -1,6 +1,7 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
@@ -72,6 +73,7 @@ public class SourceRootResourceTest {
null, null,
MockProvider.of(sourceRootResource), MockProvider.of(sourceRootResource),
null, null,
null,
null)), null)),
null); null);
dispatcher = createDispatcher(repositoryRootResource); dispatcher = createDispatcher(repositoryRootResource);