Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-09-14 17:47:45 +02:00
74 changed files with 1639 additions and 548 deletions

View File

@@ -32,6 +32,13 @@
<artifactId>scm-annotations</artifactId> <artifactId>scm-annotations</artifactId>
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- logging --> <!-- logging -->

View File

@@ -3,14 +3,8 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import java.time.Instant; public abstract class BaseMapper<T, D extends HalRepresentation> implements InstantAttributeMapper {
public abstract class BaseMapper<T, D extends HalRepresentation> {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract D map(T modelObject); public abstract D map(T modelObject);
protected Instant mapTime(Long epochMilli) {
return epochMilli == null? null: Instant.ofEpochMilli(epochMilli);
}
} }

View File

@@ -0,0 +1,9 @@
package sonia.scm.api.v2.resources;
import java.time.Instant;
public interface InstantAttributeMapper {
default Instant mapTime(Long epochMilli) {
return epochMilli == null? null: Instant.ofEpochMilli(epochMilli);
}
}

View File

@@ -33,9 +33,8 @@ package sonia.scm.plugin;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
import java.net.URL;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import java.net.URL;
/** /**
* The WebResourceLoader is able to load web resources. The resources are loaded * The WebResourceLoader is able to load web resources. The resources are loaded

View File

@@ -41,7 +41,6 @@ import sonia.scm.util.ValidationUtil;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@@ -84,12 +83,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
*/ */
private String id; private String id;
/**
* List of files changed by this changeset
*/
@XmlElement(name = "modifications")
private Modifications modifications;
/** /**
* parent changeset ids * parent changeset ids
*/ */
@@ -137,7 +130,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
&& Objects.equal(parents, other.parents) && Objects.equal(parents, other.parents)
&& Objects.equal(tags, other.tags) && Objects.equal(tags, other.tags)
&& Objects.equal(branches, other.branches) && Objects.equal(branches, other.branches)
&& Objects.equal(modifications, other.modifications)
&& Objects.equal(properties, other.properties); && Objects.equal(properties, other.properties);
//J+ //J+
} }
@@ -152,7 +144,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
public int hashCode() public int hashCode()
{ {
return Objects.hashCode(id, date, author, description, parents, tags, return Objects.hashCode(id, date, author, description, parents, tags,
branches, modifications, properties); branches, properties);
} }
/** /**
@@ -184,11 +176,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
out.append("branches: ").append(Util.toString(branches)).append("\n"); out.append("branches: ").append(Util.toString(branches)).append("\n");
out.append("tags: ").append(Util.toString(tags)).append("\n"); out.append("tags: ").append(Util.toString(tags)).append("\n");
if (modifications != null)
{
out.append("modifications: \n").append(modifications);
}
return out.toString(); return out.toString();
} }
@@ -285,21 +272,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
} }
/**
* Returns the file modifications, which was done with this changeset.
*
*
* @return file modifications
*/
public Modifications getModifications()
{
if (modifications == null)
{
modifications = new Modifications();
}
return modifications;
}
/** /**
* Return the ids of the parent changesets. * Return the ids of the parent changesets.
@@ -402,17 +374,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
this.id = id; this.id = id;
} }
/**
* Sets the file modification of the changeset.
*
*
* @param modifications file modifications
*/
public void setModifications(Modifications modifications)
{
this.modifications = modifications;
}
/** /**
* Sets the parents of the changeset. * Sets the parents of the changeset.
* *

View File

@@ -36,15 +36,13 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringEscapeUtils;
import sonia.scm.util.Util; import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.List; import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -200,4 +198,10 @@ public final class EscapeUtil
return values; return values;
} }
public static void escape(Modifications modifications) {
modifications.setAdded(escapeList(modifications.getAdded()));
modifications.setModified(escapeList(modifications.getModified()));
modifications.setRemoved(escapeList(modifications.getRemoved()));
}
} }

View File

@@ -67,7 +67,8 @@ public class Modifications implements Serializable
* Constructs ... * Constructs ...
* *
*/ */
public Modifications() {} public Modifications() {
}
/** /**
* Constructs ... * Constructs ...
@@ -218,6 +219,10 @@ public class Modifications implements Serializable
return removed; return removed;
} }
public String getRevision() {
return revision;
}
//~--- set methods ---------------------------------------------------------- //~--- set methods ----------------------------------------------------------
/** /**
@@ -253,8 +258,14 @@ public class Modifications implements Serializable
this.removed = removed; this.removed = removed;
} }
public void setRevision(String revision) {
this.revision = revision;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
private String revision;
/** list of added files */ /** list of added files */
@XmlElement(name = "added") @XmlElement(name = "added")
@XmlElementWrapper(name = "added") @XmlElementWrapper(name = "added")

View File

@@ -0,0 +1,23 @@
package sonia.scm.repository;
import sonia.scm.plugin.ExtensionPoint;
/**
* A pre processor for {@link Modifications} objects. A pre processor is able to
* modify the object before it is delivered to the user interface.
*
* @author Mohamed Karray
* @since 2.0
*/
@ExtensionPoint
public interface ModificationsPreProcessor extends PreProcessor<Modifications> {
/**
* Process the given modifications.
*
* @param modifications modifications to process
*/
@Override
void process(Modifications modifications);
}

View File

@@ -0,0 +1,26 @@
package sonia.scm.repository;
import sonia.scm.plugin.ExtensionPoint;
/**
* This factory create a {@link ModificationsPreProcessor}
*
* @author Mohamed Karray
* @since 2.0
*/
@ExtensionPoint
public interface ModificationsPreProcessorFactory extends PreProcessorFactory<Modifications> {
/**
* Create a new {@link ModificationsPreProcessor} for the given repository.
*
*
* @param repository repository
*
* @return {@link ModificationsPreProcessor} for the given repository
*/
@Override
ModificationsPreProcessor createPreProcessor(Repository repository);
}

View File

@@ -36,17 +36,15 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject; import com.google.inject.Inject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.util.Util; import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -73,14 +71,18 @@ public class PreProcessorUtil
* @param fileObjectPreProcessorFactorySet * @param fileObjectPreProcessorFactorySet
* @param blameLinePreProcessorSet * @param blameLinePreProcessorSet
* @param blameLinePreProcessorFactorySet * @param blameLinePreProcessorFactorySet
* @param modificationsPreProcessorFactorySet
* @param modificationsPreProcessorSet
*/ */
@Inject @Inject
public PreProcessorUtil(Set<ChangesetPreProcessor> changesetPreProcessorSet, public PreProcessorUtil(Set<ChangesetPreProcessor> changesetPreProcessorSet,
Set<ChangesetPreProcessorFactory> changesetPreProcessorFactorySet, Set<ChangesetPreProcessorFactory> changesetPreProcessorFactorySet,
Set<FileObjectPreProcessor> fileObjectPreProcessorSet, Set<FileObjectPreProcessor> fileObjectPreProcessorSet,
Set<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet, Set<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet,
Set<BlameLinePreProcessor> blameLinePreProcessorSet, Set<BlameLinePreProcessor> blameLinePreProcessorSet,
Set<BlameLinePreProcessorFactory> blameLinePreProcessorFactorySet) Set<BlameLinePreProcessorFactory> blameLinePreProcessorFactorySet,
Set<ModificationsPreProcessorFactory> modificationsPreProcessorFactorySet,
Set<ModificationsPreProcessor> modificationsPreProcessorSet)
{ {
this.changesetPreProcessorSet = changesetPreProcessorSet; this.changesetPreProcessorSet = changesetPreProcessorSet;
this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet; this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet;
@@ -88,6 +90,8 @@ public class PreProcessorUtil
this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet; this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet;
this.blameLinePreProcessorSet = blameLinePreProcessorSet; this.blameLinePreProcessorSet = blameLinePreProcessorSet;
this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet; this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet;
this.modificationsPreProcessorFactorySet = modificationsPreProcessorFactorySet;
this.modificationsPreProcessorSet = modificationsPreProcessorSet;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -108,13 +112,7 @@ public class PreProcessorUtil
} }
EscapeUtil.escape(blameLine); EscapeUtil.escape(blameLine);
handlePreProcess(repository,blameLine,blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
PreProcessorHandler<BlameLine> handler =
new PreProcessorHandler<BlameLine>(blameLinePreProcessorFactorySet,
blameLinePreProcessorSet, repository);
handler.callPreProcessors(blameLine);
handler.callPreProcessorFactories(blameLine);
} }
/** /**
@@ -152,13 +150,7 @@ public class PreProcessorUtil
{ {
EscapeUtil.escape(blameResult); EscapeUtil.escape(blameResult);
} }
handlePreProcessForIterable(repository, blameResult.getBlameLines(),blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
PreProcessorHandler<BlameLine> handler =
new PreProcessorHandler<BlameLine>(blameLinePreProcessorFactorySet,
blameLinePreProcessorSet, repository);
handler.callPreProcessors(blameResult.getBlameLines());
handler.callPreProcessorFactories(blameResult.getBlameLines());
} }
/** /**
@@ -183,26 +175,20 @@ public class PreProcessorUtil
* *
* @since 1.35 * @since 1.35
*/ */
public void prepareForReturn(Repository repository, Changeset changeset, public void prepareForReturn(Repository repository, Changeset changeset, boolean escape){
boolean escape) logger.trace("prepare changeset {} of repository {} for return", changeset.getId(), repository.getName());
{ if (escape) {
if (logger.isTraceEnabled())
{
logger.trace("prepare changeset {} of repository {} for return",
changeset.getId(), repository.getName());
}
if (escape)
{
EscapeUtil.escape(changeset); EscapeUtil.escape(changeset);
} }
handlePreProcess(repository, changeset, changesetPreProcessorFactorySet, changesetPreProcessorSet);
}
PreProcessorHandler<Changeset> handler = public void prepareForReturn(Repository repository, Modifications modifications, boolean escape) {
new PreProcessorHandler<Changeset>(changesetPreProcessorFactorySet, logger.trace("prepare modifications {} of repository {} for return", modifications, repository.getName());
changesetPreProcessorSet, repository); if (escape) {
EscapeUtil.escape(modifications);
handler.callPreProcessors(changeset); }
handler.callPreProcessorFactories(changeset); handlePreProcess(repository, modifications, modificationsPreProcessorFactorySet, modificationsPreProcessorSet);
} }
/** /**
@@ -240,13 +226,7 @@ public class PreProcessorUtil
{ {
EscapeUtil.escape(result); EscapeUtil.escape(result);
} }
handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
PreProcessorHandler<FileObject> handler =
new PreProcessorHandler<FileObject>(fileObjectPreProcessorFactorySet,
fileObjectPreProcessorSet, repository);
handler.callPreProcessors(result);
handler.callPreProcessorFactories(result);
} }
/** /**
@@ -272,13 +252,7 @@ public class PreProcessorUtil
{ {
EscapeUtil.escape(result); EscapeUtil.escape(result);
} }
handlePreProcessForIterable(repository,result,changesetPreProcessorFactorySet, changesetPreProcessorSet);
PreProcessorHandler<Changeset> handler =
new PreProcessorHandler<Changeset>(changesetPreProcessorFactorySet,
changesetPreProcessorSet, repository);
handler.callPreProcessors(result);
handler.callPreProcessorFactories(result);
} }
/** /**
@@ -294,6 +268,23 @@ public class PreProcessorUtil
prepareForReturn(repository, result, true); prepareForReturn(repository, result, true);
} }
private <T, F extends PreProcessorFactory<T>, P extends PreProcessor<T>> void handlePreProcess(Repository repository, T processedObject,
Collection<F> factories,
Collection<P> preProcessors) {
PreProcessorHandler<T> handler = new PreProcessorHandler<T>(factories, preProcessors, repository);
handler.callPreProcessors(processedObject);
handler.callPreProcessorFactories(processedObject);
}
private <T, I extends Iterable<T>, F extends PreProcessorFactory<T>, P extends PreProcessor<T>> void handlePreProcessForIterable(Repository repository, I processedObjects,
Collection<F> factories,
Collection<P> preProcessors) {
PreProcessorHandler<T> handler = new PreProcessorHandler<T>(factories, preProcessors, repository);
handler.callPreProcessors(processedObjects);
handler.callPreProcessorFactories(processedObjects);
}
//~--- inner classes -------------------------------------------------------- //~--- inner classes --------------------------------------------------------
/** /**
@@ -454,6 +445,10 @@ public class PreProcessorUtil
/** Field description */ /** Field description */
private final Collection<ChangesetPreProcessor> changesetPreProcessorSet; private final Collection<ChangesetPreProcessor> changesetPreProcessorSet;
private final Collection<ModificationsPreProcessorFactory> modificationsPreProcessorFactorySet;
private final Collection<ModificationsPreProcessor> modificationsPreProcessorSet;
/** Field description */ /** Field description */
private final Collection<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet; private final Collection<FileObjectPreProcessorFactory> fileObjectPreProcessorFactorySet;

View File

@@ -61,5 +61,11 @@ public enum Command
/** /**
* @since 1.43 * @since 1.43
*/ */
BUNDLE, UNBUNDLE; BUNDLE, UNBUNDLE,
/**
* @since 2.0
*/
MODIFICATIONS
} }

View File

@@ -0,0 +1,113 @@
package sonia.scm.repository.api;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.cache.Cache;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.ModificationsCommand;
import sonia.scm.repository.spi.ModificationsCommandRequest;
import java.io.IOException;
/**
* Get the modifications applied to files in a revision.
* <p>
* Modifications are for example: Add, Update and Delete
*
* @author Mohamed Karray
* @since 2.0
*/
@Slf4j
@RequiredArgsConstructor
@Accessors(fluent = true)
public final class ModificationsCommandBuilder {
static final String CACHE_NAME = "sonia.cache.cmd.modifications";
private final ModificationsCommand modificationsCommand;
private final ModificationsCommandRequest request = new ModificationsCommandRequest();
private final Repository repository;
private final Cache<ModificationsCommandBuilder.CacheKey, Modifications> cache;
private final PreProcessorUtil preProcessorUtil;
@Setter
private boolean disableEscaping = false;
@Setter
private boolean disableCache = false;
@Setter
private boolean disablePreProcessors = false;
public ModificationsCommandBuilder revision(String revision){
request.setRevision(revision);
return this;
}
/**
* Reset each parameter to its default value.
*
*
* @return {@code this}
*/
public ModificationsCommandBuilder reset() {
request.reset();
this.disableCache = false;
this.disablePreProcessors = false;
return this;
}
public Modifications getModifications() throws IOException, RevisionNotFoundException {
Modifications modifications;
if (disableCache) {
log.info("Get modifications for {} with disabled cache", request);
modifications = modificationsCommand.getModifications(request);
} else {
ModificationsCommandBuilder.CacheKey key = new ModificationsCommandBuilder.CacheKey(repository.getId(), request);
if (cache.contains(key)) {
modifications = cache.get(key);
log.debug("Get modifications for {} from the cache", request);
} else {
log.info("Get modifications for {} with enabled cache", request);
modifications = modificationsCommand.getModifications(request);
if (modifications != null) {
cache.put(key, modifications);
log.debug("Modifications for {} added to the cache with key {}", request, key);
}
}
}
if (!disablePreProcessors && (modifications != null)) {
preProcessorUtil.prepareForReturn(repository, modifications, !disableEscaping);
}
return modifications;
}
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@ToString
class CacheKey implements RepositoryCacheKey {
private final String repositoryId;
private final ModificationsCommandRequest request;
@Override
public String getRepositoryId() {
return repositoryId;
}
}
}

View File

@@ -253,6 +253,18 @@ public final class RepositoryService implements Closeable {
repository, preProcessorUtil); repository, preProcessorUtil);
} }
/**
* The modification command shows file modifications in a revision.
*
* @return instance of {@link ModificationsCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
*/
public ModificationsCommandBuilder getModificationsCommand() {
logger.debug("create modifications command for repository {}",repository.getNamespaceAndName());
return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
}
/** /**
* The outgoing command show {@link Changeset}s not found in a remote repository. * The outgoing command show {@link Changeset}s not found in a remote repository.
* *

View File

@@ -1,4 +1,4 @@
/** /*
* Copyright (c) 2010, Sebastian Sdorra * Copyright (c) 2010, Sebastian Sdorra
* All rights reserved. * All rights reserved.
* *
@@ -29,60 +29,25 @@
* *
*/ */
package sonia.scm.repository.spi;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.RevisionNotFoundException;
package sonia.scm.repository; import java.io.IOException;
//~--- non-JDK imports --------------------------------------------------------
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.wc.admin.ISVNChangeEntryHandler;
import org.tmatesoft.svn.core.wc.admin.SVNChangeEntry;
/** /**
* Command to get the modifications applied to files in a revision.
* *
* @author Sebastian Sdorra * Modifications are for example: Add, Update, Delete
*
* @author Mohamed Karray
* @since 2.0
*/ */
public class SvnModificationHandler implements ISVNChangeEntryHandler public interface ModificationsCommand {
{
/** Modifications getModifications(String revision) throws IOException, RevisionNotFoundException;
* Constructs ...
*
*
* @param changeset
*/
public SvnModificationHandler(Changeset changeset)
{
this.changeset = changeset;
}
//~--- methods -------------------------------------------------------------- Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException;
/**
* Method description
*
*
* @param entry
*
* @throws SVNException
*/
@Override
public void handleEntry(SVNChangeEntry entry) throws SVNException
{
Modifications modification = changeset.getModifications();
if (modification == null)
{
modification = new Modifications();
changeset.setModifications(modification);
}
SvnUtil.appendModification(modification, entry);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private Changeset changeset;
} }

View File

@@ -0,0 +1,24 @@
package sonia.scm.repository.spi;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@ToString
@EqualsAndHashCode
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ModificationsCommandRequest implements Resetable {
private String revision;
@Override
public void reset() {
revision = null;
}
}

View File

@@ -42,6 +42,8 @@ import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -168,6 +170,16 @@ public abstract class RepositoryServiceProvider implements Closeable
throw new CommandNotSupportedException(Command.LOG); throw new CommandNotSupportedException(Command.LOG);
} }
/**
* Get the corresponding {@link ModificationsCommand} implemented from the Plugins
*
* @return the corresponding {@link ModificationsCommand} implemented from the Plugins
* @throws CommandNotSupportedException if there is no Implementation
*/
public ModificationsCommand getModificationsCommand() {
throw new CommandNotSupportedException(Command.MODIFICATIONS);
}
/** /**
* Method description * Method description
* *

View File

@@ -21,6 +21,7 @@ public class VndMediaType {
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 = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;;
public static final String TAG = PREFIX + "tag" + SUFFIX; public static final String TAG = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX;

View File

@@ -3,6 +3,8 @@ package sonia.scm.it;
import io.restassured.response.ExtractableResponse; import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response; import io.restassured.response.Response;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Maps;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -17,8 +19,11 @@ import sonia.scm.web.VndMediaType;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import static java.lang.Thread.sleep; import static java.lang.Thread.sleep;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -312,5 +317,163 @@ public class RepositoryAccessITCase {
} }
); );
} }
@Test
@SuppressWarnings("unchecked")
public void shouldFindAddedModifications() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
String fileName = "a.txt";
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(addedFiles -> assertThat(addedFiles)
.hasSize(1)
.containsExactly(fileName))
.assertRemoved(removedFiles -> assertThat(removedFiles)
.hasSize(0))
.assertModified(files -> assertThat(files)
.hasSize(0));
}
@Test
@SuppressWarnings("unchecked")
public void shouldFindRemovedModifications() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
String fileName = "a.txt";
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName);
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertRemoved(removedFiles -> assertThat(removedFiles)
.hasSize(1)
.containsExactly(fileName))
.assertAdded(addedFiles -> assertThat(addedFiles)
.hasSize(0))
.assertModified(files -> assertThat(files)
.hasSize(0));
}
@Test
@SuppressWarnings("unchecked")
public void shouldFindUpdateModifications() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
String fileName = "a.txt";
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content");
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertModified(modifiedFiles -> assertThat(modifiedFiles)
.hasSize(1)
.containsExactly(fileName))
.assertRemoved(removedFiles -> assertThat(removedFiles)
.hasSize(0))
.assertAdded(addedFiles -> assertThat(addedFiles)
.hasSize(0));
}
@Test
@SuppressWarnings("unchecked")
public void shouldFindMultipleModifications() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "b.txt", "b");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "c.txt", "c");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "d.txt", "d");
Map<String, String> addedFiles = new HashMap<String, String>()
{{
put("a.txt", "bla bla");
}};
Map<String, String> modifiedFiles = new HashMap<String, String>()
{{
put("b.txt", "new content");
}};
ArrayList<String> removedFiles = Lists.newArrayList("c.txt", "d.txt");
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(a -> assertThat(a)
.hasSize(1)
.containsExactly("a.txt"))
.assertModified(m-> assertThat(m)
.hasSize(1)
.containsExactly("b.txt"))
.assertRemoved(r -> assertThat(r)
.hasSize(2)
.containsExactly("c.txt", "d.txt"));
}
@Test
@SuppressWarnings("unchecked")
public void svnShouldCreateOneModificationPerFolder() throws IOException {
Assume.assumeThat(repositoryType, equalTo("svn"));
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "bbb/bb/b.txt", "b");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ccc/cc/c.txt", "c");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ddd/dd/d.txt", "d");
Map<String, String> addedFiles = new HashMap<String, String>()
{{
put("aaa/aa/a.txt", "bla bla");
}};
Map<String, String> modifiedFiles = new HashMap<String, String>()
{{
put("bbb/bb/b.txt", "new content");
}};
ArrayList<String> removedFiles = Lists.newArrayList("ccc/cc/c.txt", "ddd/dd/d.txt");
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(a -> assertThat(a)
.hasSize(3)
.containsExactly("aaa/aa/a.txt", "aaa", "aaa/aa"))
.assertModified(m-> assertThat(m)
.hasSize(1)
.containsExactly("bbb/bb/b.txt"))
.assertRemoved(r -> assertThat(r)
.hasSize(2)
.containsExactly("ccc/cc/c.txt", "ddd/dd/d.txt"));
}
} }

View File

@@ -156,7 +156,7 @@ public class RepositoryRequests {
return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href")); return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href"));
} }
AppliedGetChangesetsRequest requestChangesets(String fileName) { AppliedGetChangesetsRequest requestChangesets() {
return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href")); return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href"));
} }
} }
@@ -189,6 +189,9 @@ public class RepositoryRequests {
return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href")); return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
} }
public AppliedGetModificationsRequest requestModifications(String revision) {
return new AppliedGetModificationsRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
}
} }
class AppliedGetSourcesRequest extends AppliedGetRequest<AppliedGetSourcesRequest> { class AppliedGetSourcesRequest extends AppliedGetRequest<AppliedGetSourcesRequest> {
@@ -246,4 +249,45 @@ public class RepositoryRequests {
return new GivenWithUrlAndAuth(); return new GivenWithUrlAndAuth();
} }
} }
class AppliedGetModificationsRequest extends AppliedGetRequest<AppliedGetModificationsRequest> {
public AppliedGetModificationsRequest(Response response) { super(response); }
ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String > added = resource.then().extract().path("added");
assertAdded.accept(added);
return this;
}
ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String > removed = resource.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String > modified = resource.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
} }

View File

@@ -14,6 +14,8 @@ import sonia.scm.repository.client.api.RepositoryClientFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class RepositoryUtil { public class RepositoryUtil {
@@ -42,11 +44,53 @@ public class RepositoryUtil {
} }
static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
writeAndAddFile(repositoryClient, fileName, content);
return commit(repositoryClient, username, "added " + fileName);
}
/**
* Bundle multiple File modification in one changeset
*
* @param repositoryClient
* @param username
* @param addedFiles map.key: path of the file, value: the file content
* @param modifiedFiles map.key: path of the file, value: the file content
* @param removedFiles list of file paths to be removed
* @return the changeset with all modifications
* @throws IOException
*/
static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
for (String fileName : addedFiles.keySet()) {
writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName));
}
for (String fileName : modifiedFiles.keySet()) {
writeAndAddFile(repositoryClient, fileName, modifiedFiles.get(fileName));
}
for (String fileName : removedFiles) {
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
}
return commit(repositoryClient, username, "multiple file modifications" );
}
private static File writeAndAddFile(RepositoryClient repositoryClient, String fileName, String content) throws IOException {
File file = new File(repositoryClient.getWorkingCopy(), fileName); File file = new File(repositoryClient.getWorkingCopy(), fileName);
Files.createParentDirs(file); Files.createParentDirs(file);
Files.write(content, file, Charsets.UTF_8); Files.write(content, file, Charsets.UTF_8);
addWithParentDirectories(repositoryClient, file); addWithParentDirectories(repositoryClient, file);
return commit(repositoryClient, username, "added " + fileName); return file;
}
static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
return commit(repositoryClient, username, "removed " + fileName);
}
private static void deleteFileAndApplyRemoveCommand(RepositoryClient repositoryClient, String fileName) throws IOException {
File file = new File(repositoryClient.getWorkingCopy(), fileName);
if (repositoryClient.isCommandSupported(ClientCommand.REMOVE)) {
repositoryClient.getRemoveCommand().remove(fileName);
}
file.delete();
} }
private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException { private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException {

View File

@@ -37,32 +37,25 @@ package sonia.scm.repository;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.util.Util; import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -224,13 +217,6 @@ public class GitChangesetConverter implements Closeable
changeset.setParents(parentList); changeset.setParents(parentList);
} }
Modifications modifications = createModifications(treeWalk, commit);
if (modifications != null)
{
changeset.setModifications(modifications);
}
Collection<String> tagCollection = tags.get(commit.getId()); Collection<String> tagCollection = tags.get(commit.getId());
if (Util.isNotEmpty(tagCollection)) if (Util.isNotEmpty(tagCollection))
@@ -245,108 +231,7 @@ public class GitChangesetConverter implements Closeable
return changeset; return changeset;
} }
/**
* TODO: copy and rename
*
*
* @param modifications
* @param entry
*/
private void appendModification(Modifications modifications, DiffEntry entry)
{
switch (entry.getChangeType())
{
case ADD :
modifications.getAdded().add(entry.getNewPath());
break;
case MODIFY :
modifications.getModified().add(entry.getNewPath());
break;
case DELETE :
modifications.getRemoved().add(entry.getOldPath());
break;
}
}
/**
* Method description
*
*
* @param treeWalk
* @param commit
*
* @return
*
* @throws IOException
*/
private Modifications createModifications(TreeWalk treeWalk, RevCommit commit)
throws IOException
{
Modifications modifications = null;
treeWalk.reset();
treeWalk.setRecursive(true);
if (commit.getParentCount() > 0)
{
RevCommit parent = commit.getParent(0);
RevTree tree = parent.getTree();
if ((tree == null) && (revWalk != null))
{
revWalk.parseHeaders(parent);
tree = parent.getTree();
}
if (tree != null)
{
treeWalk.addTree(tree);
}
else
{
if (logger.isTraceEnabled())
{
logger.trace("no parent tree at position 0 for commit {}",
commit.getName());
}
treeWalk.addTree(new EmptyTreeIterator());
}
}
else
{
if (logger.isTraceEnabled())
{
logger.trace("no parent available for commit {}", commit.getName());
}
treeWalk.addTree(new EmptyTreeIterator());
}
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
for (DiffEntry e : entries)
{
if (!e.getOldId().equals(e.getNewId()))
{
if (modifications == null)
{
modifications = new Modifications();
}
appendModification(modifications, e);
}
}
return modifications;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,106 @@
package sonia.scm.repository.spi;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Repository;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
@Slf4j
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
protected GitModificationsCommand(GitContext context, Repository repository) {
super(context, repository);
}
private Modifications createModifications(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk, String revision)
throws IOException, UnsupportedModificationTypeException {
treeWalk.reset();
treeWalk.setRecursive(true);
if (commit.getParentCount() > 0) {
RevCommit parent = commit.getParent(0);
RevTree tree = parent.getTree();
if ((tree == null) && (revWalk != null)) {
revWalk.parseHeaders(parent);
tree = parent.getTree();
}
if (tree != null) {
treeWalk.addTree(tree);
} else {
log.trace("no parent tree at position 0 for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
} else {
log.trace("no parent available for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
Modifications modifications = new Modifications();
for (DiffEntry e : entries) {
if (!e.getOldId().equals(e.getNewId())) {
appendModification(modifications, e);
}
}
modifications.setRevision(revision);
return modifications;
}
@Override
public Modifications getModifications(String revision) {
org.eclipse.jgit.lib.Repository gitRepository = null;
RevWalk revWalk = null;
try {
gitRepository = open();
if (!gitRepository.getAllRefs().isEmpty()) {
revWalk = new RevWalk(gitRepository);
ObjectId id = GitUtil.getRevisionId(gitRepository, revision);
RevCommit commit = revWalk.parseCommit(id);
TreeWalk treeWalk = new TreeWalk(gitRepository);
return createModifications(treeWalk, commit, revWalk, revision);
}
} catch (IOException ex) {
log.error("could not open repository", ex);
throw new InternalRepositoryException(ex);
} catch (UnsupportedModificationTypeException ex) {
log.error("Unsupported modification type", ex);
throw new InternalRepositoryException(ex);
} finally {
GitUtil.release(revWalk);
GitUtil.close(gitRepository);
}
return null;
}
@Override
public Modifications getModifications(ModificationsCommandRequest request) {
return getModifications(request.getRevision());
}
private void appendModification(Modifications modifications, DiffEntry entry) throws UnsupportedModificationTypeException {
DiffEntry.ChangeType type = entry.getChangeType();
if (type == DiffEntry.ChangeType.ADD) {
modifications.getAdded().add(entry.getNewPath());
} else if (type == DiffEntry.ChangeType.MODIFY) {
modifications.getModified().add(entry.getNewPath());
} else if (type == DiffEntry.ChangeType.DELETE) {
modifications.getRemoved().add(entry.getOldPath());
} else {
throw new UnsupportedModificationTypeException(MessageFormat.format("The modification type: {0} is not supported.", type));
}
}
}

View File

@@ -41,6 +41,8 @@ import sonia.scm.repository.api.Command;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -173,6 +175,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitLogCommand(context, repository); return new GitLogCommand(context, repository);
} }
@Override
public ModificationsCommand getModificationsCommand() {
return new GitModificationsCommand(context,repository);
}
/** /**
* Method description * Method description
* *

View File

@@ -0,0 +1,9 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.InternalRepositoryException;
public class UnsupportedModificationTypeException extends InternalRepositoryException {
public UnsupportedModificationTypeException(String message) {
super(message);
}
}

View File

@@ -85,14 +85,14 @@ public class AbstractRemoteCommandTestBase
outgoingDirectory = tempFolder.newFile("outgoing"); outgoingDirectory = tempFolder.newFile("outgoing");
outgoingDirectory.delete(); outgoingDirectory.delete();
incomgingRepository = new Repository("1", "git", "space", "incoming"); incomingRepository = new Repository("1", "git", "space", "incoming");
outgoingRepository = new Repository("2", "git", "space", "outgoing"); outgoingRepository = new Repository("2", "git", "space", "outgoing");
incoming = Git.init().setDirectory(incomingDirectory).setBare(false).call(); incoming = Git.init().setDirectory(incomingDirectory).setBare(false).call();
outgoing = Git.init().setDirectory(outgoingDirectory).setBare(false).call(); outgoing = Git.init().setDirectory(outgoingDirectory).setBare(false).call();
handler = mock(GitRepositoryHandler.class); handler = mock(GitRepositoryHandler.class);
when(handler.getDirectory(incomgingRepository)).thenReturn( when(handler.getDirectory(incomingRepository)).thenReturn(
incomingDirectory); incomingDirectory);
when(handler.getDirectory(outgoingRepository)).thenReturn( when(handler.getDirectory(outgoingRepository)).thenReturn(
outgoingDirectory); outgoingDirectory);
@@ -211,7 +211,7 @@ public class AbstractRemoteCommandTestBase
protected GitRepositoryHandler handler; protected GitRepositoryHandler handler;
/** Field description */ /** Field description */
protected Repository incomgingRepository; protected Repository incomingRepository;
/** Field description */ /** Field description */
protected Git incoming; protected Git incoming;

View File

@@ -105,7 +105,7 @@ public class GitIncomingCommandTest
commit(outgoing, "added a"); commit(outgoing, "added a");
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomgingRepository); GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomingRepository);
PullCommandRequest req = new PullCommandRequest(); PullCommandRequest req = new PullCommandRequest();
req.setRemoteRepository(outgoingRepository); req.setRemoteRepository(outgoingRepository);
pull.pull(req); pull.pull(req);
@@ -192,6 +192,6 @@ public class GitIncomingCommandTest
private GitIncomingCommand createCommand() private GitIncomingCommand createCommand()
{ {
return new GitIncomingCommand(handler, new GitContext(incomingDirectory), return new GitIncomingCommand(handler, new GitContext(incomingDirectory),
incomgingRepository); incomingRepository);
} }
} }

View File

@@ -168,21 +168,23 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
Changeset c = command.getChangeset("435df2f061add3589cb3"); Changeset c = command.getChangeset("435df2f061add3589cb3");
assertNotNull(c); assertNotNull(c);
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c.getId()); String revision = "435df2f061add3589cb326cc64be9b9c3897ceca";
assertEquals(revision, c.getId());
assertEquals("added a and b files", c.getDescription()); assertEquals("added a and b files", c.getDescription());
checkDate(c.getDate()); checkDate(c.getDate());
assertEquals("Douglas Adams", c.getAuthor().getName()); assertEquals("Douglas Adams", c.getAuthor().getName());
assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail());
assertEquals("added a and b files", c.getDescription()); assertEquals("added a and b files", c.getDescription());
Modifications mods = c.getModifications(); GitModificationsCommand gitModificationsCommand = new GitModificationsCommand(createContext(), repository);
Modifications modifications = gitModificationsCommand.getModifications(revision);
assertNotNull(mods); assertNotNull(modifications);
assertTrue("modified list should be empty", mods.getModified().isEmpty()); assertTrue("modified list should be empty", modifications.getModified().isEmpty());
assertTrue("removed list should be empty", mods.getRemoved().isEmpty()); assertTrue("removed list should be empty", modifications.getRemoved().isEmpty());
assertFalse("added list should not be empty", mods.getAdded().isEmpty()); assertFalse("added list should not be empty", modifications.getAdded().isEmpty());
assertEquals(2, mods.getAdded().size()); assertEquals(2, modifications.getAdded().size());
assertThat(mods.getAdded(), contains("a.txt", "b.txt")); assertThat(modifications.getAdded(), contains("a.txt", "b.txt"));
} }
@Test @Test

View File

@@ -0,0 +1,126 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.Modifications;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
import static org.assertj.core.api.Java6Assertions.assertThat;
public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
private GitModificationsCommand incomingModificationsCommand;
private GitModificationsCommand outgoingModificationsCommand;
@Before
public void init() {
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory), incomingRepository);
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory), outgoingRepository);
}
@Test
public void shouldReadAddedFiles() throws Exception {
write(outgoing, outgoingDirectory, "a.txt", "bal bla");
RevCommit addedFileCommit = commit(outgoing, "add file");
String revision = addedFileCommit.getName();
Consumer<Modifications> assertModifications = assertAddedFiles("a.txt");
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
pushOutgoingAndPullIncoming();
assertModifications.accept(incomingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadModifiedFiles() throws Exception {
write(outgoing, outgoingDirectory, "a.txt", "bal bla");
commit(outgoing, "add file");
write(outgoing, outgoingDirectory, "a.txt", "modified content");
RevCommit modifiedFileCommit = commit(outgoing, "modify file");
String revision = modifiedFileCommit.getName();
Consumer<Modifications> assertModifications = assertModifiedFiles("a.txt");
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
pushOutgoingAndPullIncoming();
assertModifications.accept(incomingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadRemovedFiles() throws Exception {
String fileName = "a.txt";
write(outgoing, outgoingDirectory, fileName, "bal bla");
commit(outgoing, "add file");
File file = new File(outgoingDirectory, fileName);
file.delete();
outgoing.rm().addFilepattern(fileName).call();
RevCommit removedFileCommit = commit(outgoing, "remove file");
String revision = removedFileCommit.getName();
Consumer<Modifications> assertModifications = assertRemovedFiles(fileName);
pushOutgoingAndPullIncoming();
assertModifications.accept(incomingModificationsCommand.getModifications(revision));
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
void pushOutgoingAndPullIncoming() throws IOException {
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory),
outgoingRepository);
PushCommandRequest request = new PushCommandRequest();
request.setRemoteRepository(incomingRepository);
cmd.push(request);
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory),
incomingRepository);
PullCommandRequest pullRequest = new PullCommandRequest();
pullRequest.setRemoteRepository(incomingRepository);
pullCommand.pull(pullRequest);
}
Consumer<Modifications> assertRemovedFiles(String fileName) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(1)
.containsOnly(fileName);
};
}
Consumer<Modifications> assertModifiedFiles(String file) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(1)
.containsOnly(file);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
};
}
Consumer<Modifications> assertAddedFiles(String file) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(1)
.containsOnly(file);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
};
}
}

View File

@@ -78,7 +78,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
GitOutgoingCommand cmd = createCommand(); GitOutgoingCommand cmd = createCommand();
OutgoingCommandRequest request = new OutgoingCommandRequest(); OutgoingCommandRequest request = new OutgoingCommandRequest();
request.setRemoteRepository(incomgingRepository); request.setRemoteRepository(incomingRepository);
ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request);
@@ -98,7 +98,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
* @throws RepositoryException * @throws RepositoryException
*/ */
@Test @Test
public void testGetOutgoingChangesetsWithAllreadyPushedChanges() public void testGetOutgoingChangesetsWithAlreadyPushedChanges()
throws IOException, GitAPIException throws IOException, GitAPIException
{ {
write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); write(outgoing, outgoingDirectory, "a.txt", "content of a.txt");
@@ -110,7 +110,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
outgoingRepository); outgoingRepository);
PushCommandRequest req = new PushCommandRequest(); PushCommandRequest req = new PushCommandRequest();
req.setRemoteRepository(incomgingRepository); req.setRemoteRepository(incomingRepository);
push.push(req); push.push(req);
write(outgoing, outgoingDirectory, "b.txt", "content of b.txt"); write(outgoing, outgoingDirectory, "b.txt", "content of b.txt");
@@ -120,7 +120,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
GitOutgoingCommand cmd = createCommand(); GitOutgoingCommand cmd = createCommand();
OutgoingCommandRequest request = new OutgoingCommandRequest(); OutgoingCommandRequest request = new OutgoingCommandRequest();
request.setRemoteRepository(incomgingRepository); request.setRemoteRepository(incomingRepository);
ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request);
@@ -144,7 +144,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
GitOutgoingCommand cmd = createCommand(); GitOutgoingCommand cmd = createCommand();
OutgoingCommandRequest request = new OutgoingCommandRequest(); OutgoingCommandRequest request = new OutgoingCommandRequest();
request.setRemoteRepository(incomgingRepository); request.setRemoteRepository(incomingRepository);
ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request);

View File

@@ -78,7 +78,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
GitPushCommand cmd = createCommand(); GitPushCommand cmd = createCommand();
PushCommandRequest request = new PushCommandRequest(); PushCommandRequest request = new PushCommandRequest();
request.setRemoteRepository(incomgingRepository); request.setRemoteRepository(incomingRepository);
PushResponse response = cmd.push(request); PushResponse response = cmd.push(request);

View File

@@ -0,0 +1,32 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import java.text.MessageFormat;
public class HgModificationsCommand extends AbstractCommand implements ModificationsCommand {
HgModificationsCommand(HgCommandContext context, Repository repository) {
super(context, repository);
}
@Override
public Modifications getModifications(String revision) {
com.aragost.javahg.Repository repository = open();
HgLogChangesetCommand hgLogChangesetCommand = HgLogChangesetCommand.on(repository, getContext().getConfig());
int hgRevision = hgLogChangesetCommand.rev(revision).singleRevision();
Modifications modifications = hgLogChangesetCommand.rev(MessageFormat.format("{0}:{0}", hgRevision)).extractModifications();
modifications.setRevision(revision);
return modifications;
}
@Override
public Modifications getModifications(ModificationsCommandRequest request) {
return getModifications(request.getRevision());
}
}

View File

@@ -39,6 +39,7 @@ import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -185,6 +186,16 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
return new HgLogCommand(context, repository); return new HgLogCommand(context, repository);
} }
/**
* Get the corresponding {@link ModificationsCommand} implemented from the Plugins
*
* @return the corresponding {@link ModificationsCommand} implemented from the Plugins
* @throws CommandNotSupportedException if there is no Implementation
*/
public ModificationsCommand getModificationsCommand() {
return new HgModificationsCommand(context,repository);
}
/** /**
* Method description * Method description
* *

View File

@@ -41,21 +41,18 @@ import com.aragost.javahg.internals.AbstractCommand;
import com.aragost.javahg.internals.HgInputStream; import com.aragost.javahg.internals.HgInputStream;
import com.aragost.javahg.internals.RuntimeIOException; import com.aragost.javahg.internals.RuntimeIOException;
import com.aragost.javahg.internals.Utils; import com.aragost.javahg.internals.Utils;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.Modifications; import sonia.scm.repository.Modifications;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -251,33 +248,14 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
changeset.getProperties().put(PROPERTY_CLOSE, "true"); changeset.getProperties().put(PROPERTY_CLOSE, "true");
} }
Modifications modifications = changeset.getModifications();
String line = in.textUpTo('\n'); String line = in.textUpTo('\n');
while (line.length() > 0) {
while (line.length() > 0) if (line.startsWith("t "))
{
if (line.startsWith("a "))
{
modifications.getAdded().add(line.substring(2));
}
else if (line.startsWith("m "))
{
modifications.getModified().add(line.substring(2));
}
else if (line.startsWith("d "))
{
modifications.getRemoved().add(line.substring(2));
}
else if (line.startsWith("t "))
{ {
changeset.getTags().add(line.substring(2)); changeset.getTags().add(line.substring(2));
} }
line = in.textUpTo('\n'); line = in.textUpTo('\n');
} }
String message = in.textUpTo('\0'); String message = in.textUpTo('\0');
changeset.setDescription(message); changeset.setDescription(message);
@@ -285,6 +263,36 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
return changeset; return changeset;
} }
protected Modifications readModificationsFromStream(HgInputStream in) {
try {
boolean found = in.find(CHANGESET_PATTERN);
if (found) {
while (!in.match(CHANGESET_PATTERN)) {
return extractModifications(in);
}
}
} catch (IOException e) {
throw new RuntimeIOException(e);
}
return null;
}
private Modifications extractModifications(HgInputStream in) throws IOException {
Modifications modifications = new Modifications();
String line = in.textUpTo('\n');
while (line.length() > 0) {
if (line.startsWith("a ")) {
modifications.getAdded().add(line.substring(2));
} else if (line.startsWith("m ")) {
modifications.getModified().add(line.substring(2));
} else if (line.startsWith("d ")) {
modifications.getRemoved().add(line.substring(2));
}
line = in.textUpTo('\n');
}
return modifications;
}
/** /**
* Method description * Method description
* *

View File

@@ -38,14 +38,14 @@ package sonia.scm.repository.spi.javahg;
import com.aragost.javahg.Repository; import com.aragost.javahg.Repository;
import com.aragost.javahg.internals.HgInputStream; import com.aragost.javahg.internals.HgInputStream;
import com.aragost.javahg.internals.Utils; import com.aragost.javahg.internals.Utils;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.Modifications;
//~--- JDK imports ------------------------------------------------------------
import java.util.List; import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -106,11 +106,22 @@ public class HgLogChangesetCommand extends AbstractChangesetCommand
*/ */
public List<Changeset> execute(String... files) public List<Changeset> execute(String... files)
{ {
cmdAppend("--style", CHANGESET_EAGER_STYLE_PATH); return readListFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH));
}
HgInputStream stream = launchStream(files); /**
* Extract Modifications from the Repository files
*
* @param files repo files
* @return modifications
*/
public Modifications extractModifications(String... files) {
return readModificationsFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH));
}
return readListFromStream(stream); HgInputStream getHgInputStream(String[] files, String changesetStylePath) {
cmdAppend("--style", changesetStylePath);
return launchStream(files);
} }
/** /**
@@ -138,11 +149,7 @@ public class HgLogChangesetCommand extends AbstractChangesetCommand
*/ */
public List<Integer> loadRevisions(String... files) public List<Integer> loadRevisions(String... files)
{ {
cmdAppend("--style", CHANGESET_LAZY_STYLE_PATH); return loadRevisionsFromStream(getHgInputStream(files, CHANGESET_LAZY_STYLE_PATH));
HgInputStream stream = launchStream(files);
return loadRevisionsFromStream(stream);
} }
/** /**

View File

@@ -1,6 +1,6 @@
//@flow //@flow
import React from "react"; import React from "react";
import { Image } from "@scm-manager/ui-components"; import {Image} from "@scm-manager/ui-components";
type Props = { type Props = {
}; };

View File

@@ -32,11 +32,11 @@ package sonia.scm.repository.client.spi;
import com.aragost.javahg.Repository; import com.aragost.javahg.Repository;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.io.IOException;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import java.io.IOException;
/** /**
* Mercurial implementation of the {@link CommitCommand}. * Mercurial implementation of the {@link CommitCommand}.
* *
@@ -70,9 +70,6 @@ public class HgCommitCommand implements CommitCommand
changeset.setBranches(Lists.newArrayList(c.getBranch())); changeset.setBranches(Lists.newArrayList(c.getBranch()));
changeset.setTags(c.tags()); changeset.setTags(c.tags());
changeset.setModifications(
new Modifications(c.getAddedFiles(), c.getModifiedFiles(), c.getDeletedFiles())
);
return changeset; return changeset;
} }

View File

@@ -39,6 +39,9 @@ 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.Modifications; import sonia.scm.repository.Modifications;
import sonia.scm.repository.RevisionNotFoundException;
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;
@@ -133,27 +136,28 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
} }
@Test @Test
public void testGetCommit() { public void testGetCommit() throws IOException, RevisionNotFoundException {
HgLogCommand command = createComamnd(); HgLogCommand command = createComamnd();
String revision = "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427";
Changeset c = Changeset c =
command.getChangeset("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"); command.getChangeset(revision);
assertNotNull(c); assertNotNull(c);
assertEquals("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427", c.getId()); assertEquals(revision, c.getId());
assertEquals("added a and b files", c.getDescription()); assertEquals("added a and b files", c.getDescription());
checkDate(c.getDate()); checkDate(c.getDate());
assertEquals("Douglas Adams", c.getAuthor().getName()); assertEquals("Douglas Adams", c.getAuthor().getName());
assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail());
assertEquals("added a and b files", c.getDescription()); assertEquals("added a and b files", c.getDescription());
ModificationsCommand modificationsCommand = new HgModificationsCommand(cmdContext, repository);
Modifications modifications = modificationsCommand.getModifications(revision);
Modifications mods = c.getModifications(); assertNotNull(modifications);
assertTrue("modified list should be empty", modifications.getModified().isEmpty());
assertNotNull(mods); assertTrue("removed list should be empty", modifications.getRemoved().isEmpty());
assertTrue("modified list should be empty", mods.getModified().isEmpty()); assertFalse("added list should not be empty", modifications.getAdded().isEmpty());
assertTrue("removed list should be empty", mods.getRemoved().isEmpty()); assertEquals(2, modifications.getAdded().size());
assertFalse("added list should not be empty", mods.getAdded().isEmpty()); assertThat(modifications.getAdded(), contains("a.txt", "b.txt"));
assertEquals(2, mods.getAdded().size());
assertThat(mods.getAdded(), contains("a.txt", "b.txt"));
} }
@Test @Test

View File

@@ -0,0 +1,111 @@
package sonia.scm.repository.spi;
import com.aragost.javahg.Changeset;
import com.aragost.javahg.commands.RemoveCommand;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.Modifications;
import java.io.File;
import java.util.function.Consumer;
import static org.assertj.core.api.Java6Assertions.assertThat;
public class HgModificationsCommandTest extends IncomingOutgoingTestBase {
private HgModificationsCommand outgoingModificationsCommand;
@Before
public void init() {
HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory);
outgoingModificationsCommand = new HgModificationsCommand(outgoingContext, outgoingRepository);
}
@Test
public void shouldReadAddedFiles() throws Exception {
String fileName = "a.txt";
writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla");
Changeset changeset = commit(outgoing, "added a.txt");
String revision = String.valueOf(changeset.getRevision());
Consumer<Modifications> assertModifications = assertAddedFile(fileName);
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadModifiedFiles() throws Exception {
String fileName = "a.txt";
writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla");
commit(outgoing, "added a.txt");
writeNewFile(outgoing, outgoingDirectory, fileName, "new content");
Changeset changeset = commit(outgoing, "modified a.txt");
String revision = String.valueOf(changeset.getRevision());
Consumer<Modifications> assertModifications = assertModifiedFiles(fileName);
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
@Test
public void shouldReadRemovedFiles() throws Exception {
String fileName = "a.txt";
writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla");
commit(outgoing, "added a.txt");
File file = new File(outgoingDirectory, fileName);
file.delete();
RemoveCommand.on(outgoing).execute(file);
Changeset changeset = commit(outgoing, "removed a.txt");
String revision = String.valueOf(changeset.getRevision());
Consumer<Modifications> assertModifications = assertRemovedFiles(fileName);
assertModifications.accept(outgoingModificationsCommand.getModifications(revision));
}
Consumer<Modifications> assertRemovedFiles(String fileName) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(1)
.containsOnly(fileName);
};
}
Consumer<Modifications> assertModifiedFiles(String file) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(0);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(1)
.containsOnly(file);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
};
}
Consumer<Modifications> assertAddedFile(String addedFile) {
return (modifications) -> {
assertThat(modifications).isNotNull();
assertThat(modifications.getAdded())
.as("added files modifications")
.hasSize(1)
.containsOnly(addedFile);
assertThat(modifications.getModified())
.as("modified files modifications")
.hasSize(0);
assertThat(modifications.getRemoved())
.as("removed files modifications")
.hasSize(0);
};
}
}

View File

@@ -49,7 +49,6 @@ import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNXMLUtil; import org.tmatesoft.svn.core.internal.util.SVNXMLUtil;
import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.admin.SVNChangeEntry;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -103,30 +102,37 @@ public final class SvnUtil
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
/** public static long parseRevision(String v) throws RevisionNotFoundException {
* TODO: type replaced long result = -1l;
*
* if (!Strings.isNullOrEmpty(v))
* @param modifications {
* @param entry try
*/ {
public static void appendModification(Modifications modifications, result = Long.parseLong(v);
SVNLogEntryPath entry) }
{ catch (NumberFormatException ex)
appendModification(modifications, entry.getType(), entry.getPath()); {
throw new RevisionNotFoundException(v);
}
}
return result;
} }
/**
* Method description public static Modifications createModifications(SVNLogEntry entry, String revision) {
* Modifications modifications = new Modifications();
* modifications.setRevision(revision);
* @param modifications Map<String, SVNLogEntryPath> changeMap = entry.getChangedPaths();
* @param entry
*/ if (Util.isNotEmpty(changeMap)) {
public static void appendModification(Modifications modifications,
SVNChangeEntry entry) for (SVNLogEntryPath e : changeMap.values()) {
{ appendModification(modifications, e.getType(), e.getPath());
appendModification(modifications, entry.getType(), entry.getPath()); }
}
return modifications;
} }
/** /**
@@ -210,19 +216,6 @@ public final class SvnUtil
{ {
changeset.getParents().add(String.valueOf(revision - 1)); changeset.getParents().add(String.valueOf(revision - 1));
} }
Map<String, SVNLogEntryPath> changeMap = entry.getChangedPaths();
if (Util.isNotEmpty(changeMap))
{
Modifications modifications = changeset.getModifications();
for (SVNLogEntryPath e : changeMap.values())
{
appendModification(modifications, e);
}
}
return changeset; return changeset;
} }

View File

@@ -54,6 +54,8 @@ import sonia.scm.util.Util;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import static sonia.scm.repository.SvnUtil.parseRevision;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
public class SvnLogCommand extends AbstractSvnCommand implements LogCommand public class SvnLogCommand extends AbstractSvnCommand implements LogCommand
@@ -144,25 +146,6 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand
return changesets; return changesets;
} }
//~--- methods --------------------------------------------------------------
private long parseRevision(String v) throws RevisionNotFoundException {
long result = -1l;
if (!Strings.isNullOrEmpty(v))
{
try
{
result = Long.parseLong(v);
}
catch (NumberFormatException ex)
{
throw new RevisionNotFoundException(v);
}
}
return result;
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -0,0 +1,50 @@
package sonia.scm.repository.spi;
import lombok.extern.slf4j.Slf4j;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.io.SVNRepository;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.SvnUtil;
import sonia.scm.util.Util;
import java.io.IOException;
import java.util.Collection;
@Slf4j
public class SvnModificationsCommand extends AbstractSvnCommand implements ModificationsCommand {
SvnModificationsCommand(SvnContext context, Repository repository) {
super(context, repository);
}
@Override
@SuppressWarnings("unchecked")
public Modifications getModifications(String revision) throws IOException, RevisionNotFoundException {
Modifications modifications = null;
log.debug("get modifications {}", revision);
try {
long revisionNumber = SvnUtil.parseRevision(revision);
SVNRepository repo = open();
Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber,
revisionNumber, true, true);
if (Util.isNotEmpty(entries)) {
modifications = SvnUtil.createModifications(entries.iterator().next(), revision);
}
} catch (SVNException ex) {
throw new InternalRepositoryException("could not open repository", ex);
}
return modifications;
}
@Override
public Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException {
return getModifications(request.getRevision());
}
}

View File

@@ -37,22 +37,19 @@ package sonia.scm.repository.spi;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.wc.ISVNOptions; import org.tmatesoft.svn.core.wc.ISVNOptions;
import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNWCUtil; import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.core.wc.admin.SVNLookClient; import org.tmatesoft.svn.core.wc.admin.SVNLookClient;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.SvnModificationHandler;
import sonia.scm.repository.SvnUtil; import sonia.scm.repository.SvnUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.File; import java.io.File;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -123,10 +120,6 @@ public class SvnPreReceiveHookChangesetProvier
{ {
changeset = SvnUtil.createChangeset(entry); changeset = SvnUtil.createChangeset(entry);
changeset.setId(SvnUtil.createTransactionEntryId(transaction)); changeset.setId(SvnUtil.createTransactionEntryId(transaction));
clientManager.doGetChanged(repositoryDirectory, transaction,
new SvnModificationHandler(changeset), true);
} }
else if (logger.isWarnEnabled()) else if (logger.isWarnEnabled())
{ {

View File

@@ -154,6 +154,10 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider
return new SvnLogCommand(context, repository); return new SvnLogCommand(context, repository);
} }
public ModificationsCommand getModificationsCommand() {
return new SvnModificationsCommand(context, repository);
}
/** /**
* Method description * Method description
* *

View File

@@ -1,6 +1,6 @@
//@flow //@flow
import React from "react"; import React from "react";
import { Image } from "@scm-manager/ui-components"; import {Image} from "@scm-manager/ui-components";
type Props = { type Props = {
}; };

View File

@@ -70,13 +70,15 @@ class SvnChangeWorker {
SVNWCClient wClient = client.getWCClient(); SVNWCClient wClient = client.getWCClient();
// add files // add files
try { if (!addedFiles.isEmpty()){
wClient.doAdd(addedFiles.toArray(new File[0]), true, false, false, try {
SVNDepth.INFINITY, false, false, false); wClient.doAdd(addedFiles.toArray(new File[0]), true, false, false,
addedFiles.clear(); SVNDepth.INFINITY, false, false, false);
addedFiles.clear();
} catch (SVNException ex) { } catch (SVNException ex) {
throw new RepositoryClientException("failed to add files", ex); throw new RepositoryClientException("failed to add files", ex);
}
} }
// remove files // remove files

View File

@@ -40,6 +40,8 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.Modifications; import sonia.scm.repository.Modifications;
import sonia.scm.repository.RevisionNotFoundException; import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@@ -128,7 +130,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase
} }
@Test @Test
public void testGetCommit() throws RevisionNotFoundException { public void testGetCommit() throws RevisionNotFoundException, IOException {
Changeset c = createCommand().getChangeset("3"); Changeset c = createCommand().getChangeset("3");
assertNotNull(c); assertNotNull(c);
@@ -137,15 +139,15 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase
checkDate(c.getDate()); checkDate(c.getDate());
assertEquals("perfect", c.getAuthor().getName()); assertEquals("perfect", c.getAuthor().getName());
assertNull("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertNull("douglas.adams@hitchhiker.com", c.getAuthor().getMail());
SvnModificationsCommand modificationsCommand = new SvnModificationsCommand(createContext(), repository);
Modifications modifications = modificationsCommand.getModifications("3");
Modifications mods = c.getModifications(); assertNotNull(modifications);
assertEquals(1, modifications.getModified().size());
assertNotNull(mods); assertEquals(1, modifications.getRemoved().size());
assertEquals(1, mods.getModified().size()); assertTrue("added list should be empty", modifications.getAdded().isEmpty());
assertEquals(1, mods.getRemoved().size()); assertEquals("a.txt", modifications.getModified().get(0));
assertTrue("added list should be empty", mods.getAdded().isEmpty()); assertEquals("b.txt", modifications.getRemoved().get(0));
assertEquals("a.txt", mods.getModified().get(0));
assertEquals("b.txt", mods.getRemoved().get(0));
} }
@Test @Test

View File

@@ -254,7 +254,6 @@
<artifactId>tika-core</artifactId> <artifactId>tika-core</artifactId>
<version>1.18</version> <version>1.18</version>
</dependency> </dependency>
<!-- test scope --> <!-- test scope -->
<dependency> <dependency>

View File

@@ -1,73 +1,21 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import de.otto.edison.hal.paging.NumberedPaging;
import de.otto.edison.hal.paging.PagingRel;
import sonia.scm.ModelObject; import sonia.scm.ModelObject;
import sonia.scm.PageResult; import sonia.scm.PageResult;
import javax.inject.Inject;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
import static com.damnhandy.uri.template.UriTemplate.fromTemplate; public class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation, M extends BaseMapper<E, D>> extends PagedCollectionToDtoMapper<E, D> {
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
import static java.util.stream.Collectors.toList;
abstract class BasicCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation, M extends BaseMapper<E, D>> {
private final String collectionName;
private final M entityToDtoMapper; private final M entityToDtoMapper;
@Inject
public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) { public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) {
this.collectionName = collectionName; super(collectionName);
this.entityToDtoMapper = entityToDtoMapper; this.entityToDtoMapper = entityToDtoMapper;
} }
CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink) { CollectionDto map(int pageNumber, int pageSize, PageResult<E> pageResult, String selfLink, Optional<String> createLink) {
return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map); return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map);
} }
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(
createLinks(paging, selfLink, createLink),
embedDtos(dtos));
collectionDto.setPage(pageNumber);
collectionDto.setPageTotal(computePageTotal(pageSize, pageResult));
return collectionDto;
}
private int computePageTotal(int pageSize, PageResult<E> pageResult) {
if (pageResult.getOverallCount() % pageSize > 0) {
return pageResult.getOverallCount() / pageSize + 1;
} else {
return pageResult.getOverallCount() / pageSize;
}
}
private Links createLinks(NumberedPaging page, String selfLink, Optional<String> createLink) {
Links.Builder linksBuilder = linkingTo()
.with(page.links(
fromTemplate(selfLink + "{?page,pageSize}"),
EnumSet.allOf(PagingRel.class)));
createLink.ifPresent(link -> linksBuilder.single(link("create", link)));
return linksBuilder.build();
}
private Embedded embedDtos(List<HalRepresentation> dtos) {
return embeddedBuilder()
.with(collectionName, dtos)
.build();
}
} }

View File

@@ -8,14 +8,14 @@ import javax.inject.Inject;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper<Changeset, ChangesetDto, ChangesetToChangesetDtoMapper> { public class ChangesetCollectionToDtoMapper extends PagedCollectionToDtoMapper<Changeset, ChangesetDto> {
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
protected final ResourceLinks resourceLinks; protected final ResourceLinks resourceLinks;
@Inject @Inject
public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super("changesets", changesetToChangesetDtoMapper); super("changesets");
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
} }
@@ -32,3 +32,4 @@ public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper<C
return resourceLinks.changeset().all(repository.getNamespace(), repository.getName()); return resourceLinks.changeset().all(repository.getNamespace(), repository.getName());
} }
} }

View File

@@ -23,7 +23,7 @@ import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo; import static de.otto.edison.hal.Links.linkingTo;
@Mapper @Mapper
public abstract class ChangesetToChangesetDtoMapper extends BaseMapper<Changeset, ChangesetDto> { public abstract class ChangesetToChangesetDtoMapper implements InstantAttributeMapper {
@Inject @Inject
private RepositoryServiceFactory serviceFactory; private RepositoryServiceFactory serviceFactory;
@@ -65,7 +65,8 @@ public abstract class ChangesetToChangesetDtoMapper extends BaseMapper<Changeset
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId())) .self(resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), target.getId()))
.single(link("diff", resourceLinks.diff().self(namespace, name, target.getId()))); .single(link("diff", resourceLinks.diff().self(namespace, name, target.getId())))
.single(link("modifications", resourceLinks.modifications().self(namespace, name, target.getId())));
target.add(linksBuilder.build()); target.add(linksBuilder.build());
} }

View File

@@ -16,7 +16,7 @@ import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo; import static de.otto.edison.hal.Links.linkingTo;
@Mapper @Mapper
public abstract class ChangesetToParentDtoMapper extends BaseMapper<Changeset, ParentChangesetDto> { public abstract class ChangesetToParentDtoMapper {
@Inject @Inject
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;

View File

@@ -4,6 +4,7 @@ import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping; import org.mapstruct.AfterMapping;
import org.mapstruct.Context; import org.mapstruct.Context;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
@@ -14,11 +15,12 @@ import javax.inject.Inject;
import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Link.link;
@Mapper @Mapper
public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper<FileObject, FileObjectDto> { public abstract class FileObjectToFileObjectDtoMapper implements InstantAttributeMapper {
@Inject @Inject
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context String revision); protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context String revision);
abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); abstract SubRepositoryDto mapSubrepository(SubRepository subRepository);

View File

@@ -34,6 +34,7 @@ public class MapperModule extends AbstractModule {
bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.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());
bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass());
// no mapstruct required // no mapstruct required
bind(UIPluginDtoMapper.class); bind(UIPluginDtoMapper.class);

View File

@@ -0,0 +1,39 @@
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.util.List;
@Getter
@Setter
@NoArgsConstructor
public class ModificationsDto extends HalRepresentation {
private String revision;
/**
* list of added files
*/
private List<String> added;
/**
* list of modified files
*/
private List<String> modified;
/**
* list of removed files
*/
private List<String> removed;
@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,62 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryNotFoundException;
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.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.io.IOException;
public class ModificationsRootResource {
private final RepositoryServiceFactory serviceFactory;
private final ModificationsToDtoMapper modificationsToDtoMapper;
@Inject
public ModificationsRootResource(RepositoryServiceFactory serviceFactory, ModificationsToDtoMapper modificationsToDtoMapper) {
this.serviceFactory = serviceFactory;
this.modificationsToDtoMapper = modificationsToDtoMapper;
}
/**
* Get the file modifications related to a revision.
* file modifications are for example: Modified, Added or Removed.
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the modifications"),
@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.MODIFICATIONS)
@TypeHint(ModificationsDto.class)
@Path("{revision}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException, RevisionNotFoundException, RepositoryNotFoundException , InternalRepositoryException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Modifications modifications = repositoryService.getModificationsCommand()
.revision(revision)
.getModifications();
ModificationsDto output = modificationsToDtoMapper.map(modifications, repositoryService.getRepository());
if (modifications != null ) {
return Response.ok(output).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
}
}

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.Modifications;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class ModificationsToDtoMapper {
@Inject
private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract ModificationsDto map(Modifications modifications, @Context Repository repository);
@AfterMapping
void appendLinks(@MappingTarget ModificationsDto target, @Context Repository repository) {
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.modifications().self(repository.getNamespace(), repository.getName(), target.getRevision()));
target.add(linksBuilder.build());
}
}

View File

@@ -0,0 +1,64 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import de.otto.edison.hal.paging.NumberedPaging;
import de.otto.edison.hal.paging.PagingRel;
import sonia.scm.ModelObject;
import sonia.scm.PageResult;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import static com.damnhandy.uri.template.UriTemplate.fromTemplate;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
import static java.util.stream.Collectors.toList;
abstract class PagedCollectionToDtoMapper<E extends ModelObject, D extends HalRepresentation> {
private final String collectionName;
PagedCollectionToDtoMapper(String collectionName) {
this.collectionName = collectionName;
}
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(
createLinks(paging, selfLink, createLink),
embedDtos(dtos));
collectionDto.setPage(pageNumber);
collectionDto.setPageTotal(computePageTotal(pageSize, pageResult));
return collectionDto;
}
private int computePageTotal(int pageSize, PageResult<E> pageResult) {
if (pageResult.getOverallCount() % pageSize > 0) {
return pageResult.getOverallCount() / pageSize + 1;
} else {
return pageResult.getOverallCount() / pageSize;
}
}
private Links createLinks(NumberedPaging page, String selfLink, Optional<String> createLink) {
Links.Builder linksBuilder = linkingTo()
.with(page.links(
fromTemplate(selfLink + "{?page,pageSize}"),
EnumSet.allOf(PagingRel.class)));
createLink.ifPresent(link -> linksBuilder.single(link("create", link)));
return linksBuilder.build();
}
private Embedded embedDtos(List<HalRepresentation> dtos) {
return embeddedBuilder()
.with(collectionName, dtos)
.build();
}
}

View File

@@ -10,8 +10,6 @@ import java.util.Optional;
import static java.util.Optional.empty; import static java.util.Optional.empty;
import static java.util.Optional.of; import static java.util.Optional.of;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<Repository, RepositoryDto, RepositoryToRepositoryDtoMapper> { public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper<Repository, RepositoryDto, RepositoryToRepositoryDtoMapper> {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;

View File

@@ -40,6 +40,7 @@ public class RepositoryResource {
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; private final Provider<DiffRootResource> diffRootResource;
private final Provider<ModificationsRootResource> modificationsRootResource;
private final Provider<FileHistoryRootResource> fileHistoryRootResource; private final Provider<FileHistoryRootResource> fileHistoryRootResource;
@Inject @Inject
@@ -52,7 +53,9 @@ public class RepositoryResource {
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource, Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
Provider<PermissionRootResource> permissionRootResource, Provider<PermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource, Provider<DiffRootResource> diffRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource) { Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource
) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager; this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper; this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -64,6 +67,7 @@ public class RepositoryResource {
this.contentResource = contentResource; this.contentResource = contentResource;
this.permissionRootResource = permissionRootResource; this.permissionRootResource = permissionRootResource;
this.diffRootResource = diffRootResource; this.diffRootResource = diffRootResource;
this.modificationsRootResource = modificationsRootResource;
this.fileHistoryRootResource = fileHistoryRootResource; this.fileHistoryRootResource = fileHistoryRootResource;
} }
@@ -188,6 +192,9 @@ public class RepositoryResource {
return permissionRootResource.get(); return permissionRootResource.get();
} }
@Path("modifications/")
public ModificationsRootResource modifications() {return modificationsRootResource.get(); }
private Optional<Response> handleNotArchived(Throwable throwable) { private Optional<Response> handleNotArchived(Throwable throwable) {
if (throwable instanceof RepositoryIsNotArchivedException) { if (throwable instanceof RepositoryIsNotArchivedException) {
return Optional.of(Response.status(Response.Status.PRECONDITION_FAILED).build()); return Optional.of(Response.status(Response.Status.PRECONDITION_FAILED).build());

View File

@@ -306,6 +306,21 @@ class ResourceLinks {
} }
} }
public ModificationsLinks modifications() {
return new ModificationsLinks(scmPathInfoStore.get());
}
static class ModificationsLinks {
private final LinkBuilder modificationsLinkBuilder;
ModificationsLinks(ScmPathInfo uriInfo) {
modificationsLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, ModificationsRootResource.class);
}
String self(String namespace, String name, String revision) {
return modificationsLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("modifications").parameters().method("get").parameters(revision).href();
}
}
public FileHistoryLinks fileHistory() { public FileHistoryLinks fileHistory() {
return new FileHistoryLinks(scmPathInfoStore.get()); return new FileHistoryLinks(scmPathInfoStore.get());
} }

View File

@@ -10,8 +10,6 @@ import java.util.Optional;
import static java.util.Optional.empty; import static java.util.Optional.empty;
import static java.util.Optional.of; import static java.util.Optional.of;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto, UserToUserDtoMapper> { public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper<User, UserDto, UserToUserDtoMapper> {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;

View File

@@ -19,8 +19,15 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
public class WebResourceServletTest { public class WebResourceServletTest {

View File

@@ -45,7 +45,7 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j @Slf4j
public class BranchRootResourceTest { public class BranchRootResourceTest extends RepositoryTestBase {
public static final String BRANCH_PATH = "space/repo/branches/master"; public static final String BRANCH_PATH = "space/repo/branches/master";
public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH; public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH;
@@ -93,9 +93,8 @@ public class BranchRootResourceTest {
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(Providers.of(new RepositoryResource(null, null, null, null, Providers.of(branchRootResource), null, null, null, null, null, null)), null); super.branchRootResource = Providers.of(branchRootResource);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
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(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));

View File

@@ -45,7 +45,7 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j @Slf4j
public class ChangesetRootResourceTest { public class ChangesetRootResourceTest extends RepositoryTestBase {
public static final String CHANGESET_PATH = "space/repo/changesets/"; public static final String CHANGESET_PATH = "space/repo/changesets/";
@@ -80,10 +80,8 @@ public class ChangesetRootResourceTest {
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(Providers super.changesetRootResource = Providers.of(changesetRootResource);
.of(new RepositoryResource(null, null, null, null, null, dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
Providers.of(changesetRootResource), null, null, null, null, null)), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));

View File

@@ -38,7 +38,7 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j @Slf4j
public class DiffResourceTest { public class DiffResourceTest extends RepositoryTestBase {
public static final String DIFF_PATH = "space/repo/diff/"; public static final String DIFF_PATH = "space/repo/diff/";
@@ -64,10 +64,8 @@ public class DiffResourceTest {
@Before @Before
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
diffRootResource = new DiffRootResource(serviceFactory); diffRootResource = new DiffRootResource(serviceFactory);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(Providers super.diffRootResource = Providers.of(diffRootResource);
.of(new RepositoryResource(null, null, null, null, null, dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
null, null, null, null, Providers.of(diffRootResource),null)), null);
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(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));

View File

@@ -14,6 +14,7 @@ public class DispatcherMock {
dispatcher.getProviderFactory().registerProvider(AlreadyExistsExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AlreadyExistsExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
return dispatcher; return dispatcher;
} }
} }

View File

@@ -48,7 +48,7 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j @Slf4j
public class FileHistoryResourceTest { public class FileHistoryResourceTest extends RepositoryTestBase {
public static final String FILE_HISTORY_PATH = "space/repo/history/"; public static final String FILE_HISTORY_PATH = "space/repo/history/";
public static final String FILE_HISTORY_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + FILE_HISTORY_PATH; public static final String FILE_HISTORY_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + FILE_HISTORY_PATH;
@@ -82,10 +82,8 @@ public class FileHistoryResourceTest {
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(Providers super.fileHistoryRootResource = Providers.of(fileHistoryRootResource);
.of(new RepositoryResource(null, null, null, null, null, dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
null, null, null, null, null, Providers.of(fileHistoryRootResource))), null);
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(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));

View File

@@ -0,0 +1,150 @@
package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;
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.InternalRepositoryException;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.ModificationsCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import java.net.URI;
import java.text.MessageFormat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(MockitoJUnitRunner.Silent.class)
public class ModificationsResourceTest extends RepositoryTestBase {
public static final String MODIFICATIONS_PATH = "space/repo/modifications/";
public static final String MODIFICATIONS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + MODIFICATIONS_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 repositoryService;
@Mock
private ModificationsCommandBuilder modificationsCommandBuilder;
@InjectMocks
private ModificationsToDtoMapperImpl modificationsToDtoMapper;
private ModificationsRootResource modificationsRootResource;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@Before
public void prepareEnvironment() throws Exception {
modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper);
super.modificationsRootResource = Providers.of(modificationsRootResource);
dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
when(repositoryService.getModificationsCommand()).thenReturn(modificationsCommandBuilder);
subjectThreadState.bind();
ThreadContext.bind(subject);
when(subject.isPermitted(any(String.class))).thenReturn(true);
}
@After
public void cleanupContext() {
ThreadContext.unbindSubject();
}
@Test
public void shouldGet404OnMissingModifications() throws Exception {
when(modificationsCommandBuilder.revision(any())).thenReturn(modificationsCommandBuilder);
when(modificationsCommandBuilder.getModifications()).thenReturn(null);
MockHttpRequest request = MockHttpRequest
.get(MODIFICATIONS_URL + "not_existing_revision")
.accept(VndMediaType.MODIFICATIONS);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(404, response.getStatus());
}
@Test
public void shouldGet500OnModificationsCommandError() throws Exception {
when(modificationsCommandBuilder.revision(any())).thenReturn(modificationsCommandBuilder);
when(modificationsCommandBuilder.getModifications()).thenThrow(InternalRepositoryException.class);
MockHttpRequest request = MockHttpRequest
.get(MODIFICATIONS_URL + "revision")
.accept(VndMediaType.MODIFICATIONS);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(500, response.getStatus());
}
@Test
public void shouldGetModifications() throws Exception {
Modifications modifications = new Modifications();
String revision = "revision";
String addedFile_1 = "a.txt";
String addedFile_2 = "b.txt";
String modifiedFile_1 = "d.txt";
String modifiedFile_2 = "c.txt";
String removedFile_1 = "e.txt";
String removedFile_2 = "f.txt";
modifications.setRevision(revision);
modifications.setAdded(Lists.newArrayList(addedFile_1, addedFile_2));
modifications.setModified(Lists.newArrayList(modifiedFile_1, modifiedFile_2));
modifications.setRemoved(Lists.newArrayList(removedFile_1, removedFile_2));
when(modificationsCommandBuilder.getModifications()).thenReturn(modifications);
when(modificationsCommandBuilder.revision(eq(revision))).thenReturn(modificationsCommandBuilder);
MockHttpRequest request = MockHttpRequest
.get(MODIFICATIONS_URL + revision)
.accept(VndMediaType.MODIFICATIONS);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(200, response.getStatus());
log.info("the content: ", response.getContentAsString());
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision)));
assertTrue(response.getContentAsString().contains(MessageFormat.format("\"added\":[\"{0}\",\"{1}\"]", addedFile_1, addedFile_2)));
assertTrue(response.getContentAsString().contains(MessageFormat.format("\"modified\":[\"{0}\",\"{1}\"]", modifiedFile_1, modifiedFile_2)));
assertTrue(response.getContentAsString().contains(MessageFormat.format("\"removed\":[\"{0}\",\"{1}\"]", removedFile_1, removedFile_2)));
}
}

View File

@@ -65,7 +65,7 @@ import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
password = "secret", password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini" configuration = "classpath:sonia/scm/repository/shiro.ini"
) )
public class PermissionRootResourceTest { public class PermissionRootResourceTest extends RepositoryTestBase {
private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAMESPACE = "repo_namespace";
private static final String REPOSITORY_NAME = "repo"; private static final String REPOSITORY_NAME = "repo";
private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME; private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME;
@@ -138,9 +138,8 @@ public class PermissionRootResourceTest {
initMocks(this); initMocks(this);
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(Providers super.permissionRootResource = Providers.of(permissionRootResource);
.of(new RepositoryResource(null, null, null, null, null, null, null, null, Providers.of(permissionRootResource), null, null)), null); dispatcher = createDispatcher(getRepositoryRootResource());
dispatcher = createDispatcher(repositoryRootResource);
subjectThreadState.bind(); subjectThreadState.bind();
ThreadContext.bind(subject); ThreadContext.bind(subject);
} }

View File

@@ -57,7 +57,7 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
password = "secret", password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini" configuration = "classpath:sonia/scm/repository/shiro.ini"
) )
public class RepositoryRootResourceTest { public class RepositoryRootResourceTest extends RepositoryTestBase {
private Dispatcher dispatcher; private Dispatcher dispatcher;
@@ -87,14 +87,15 @@ 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, null, null); super.repositoryToDtoMapper = repositoryToDtoMapper;
super.dtoToRepositoryMapper = dtoToRepositoryMapper;
super.manager = repositoryManager;
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks));
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(Providers.of(repositoryResource), Providers.of(repositoryCollectionResource)); dispatcher = createDispatcher(getRepositoryRootResource());
when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo); when(scmPathInfoStore.get()).thenReturn(uriInfo);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
dispatcher = createDispatcher(repositoryRootResource);
} }
@Test @Test

View File

@@ -0,0 +1,43 @@
package sonia.scm.api.v2.resources;
import com.google.inject.util.Providers;
import sonia.scm.repository.RepositoryManager;
import javax.inject.Provider;
public abstract class RepositoryTestBase {
protected RepositoryToRepositoryDtoMapper repositoryToDtoMapper;
protected RepositoryDtoToRepositoryMapper dtoToRepositoryMapper;
protected RepositoryManager manager;
protected Provider<TagRootResource> tagRootResource;
protected Provider<BranchRootResource> branchRootResource;
protected Provider<ChangesetRootResource> changesetRootResource;
protected Provider<SourceRootResource> sourceRootResource;
protected Provider<ContentResource> contentResource;
protected Provider<PermissionRootResource> permissionRootResource;
protected Provider<DiffRootResource> diffRootResource;
protected Provider<ModificationsRootResource> modificationsRootResource;
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
protected Provider<RepositoryCollectionResource> repositoryCollectionResource;
RepositoryRootResource getRepositoryRootResource() {
return new RepositoryRootResource(Providers.of(new RepositoryResource(
repositoryToDtoMapper,
dtoToRepositoryMapper,
manager,
tagRootResource,
branchRootResource,
changesetRootResource,
sourceRootResource,
contentResource,
permissionRootResource,
diffRootResource,
modificationsRootResource,
fileHistoryRootResource)), repositoryCollectionResource);
}
}

View File

@@ -27,6 +27,7 @@ public class ResourceLinksMock {
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.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo));
when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(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

@@ -33,7 +33,7 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
public class SourceRootResourceTest { public class SourceRootResourceTest extends RepositoryTestBase {
private Dispatcher dispatcher; private Dispatcher dispatcher;
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
@@ -64,20 +64,8 @@ public class SourceRootResourceTest {
when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto); when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto);
SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper); SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper);
RepositoryRootResource repositoryRootResource = super.sourceRootResource = Providers.of(sourceRootResource);
new RepositoryRootResource(Providers.of(new RepositoryResource(null, dispatcher = createDispatcher(getRepositoryRootResource());
null,
null,
null,
null,
null,
Providers.of(sourceRootResource),
null,
null,
null,
null)),
null);
dispatcher = createDispatcher(repositoryRootResource);
} }
@Test @Test

View File

@@ -8,7 +8,6 @@ import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState; import org.apache.shiro.util.ThreadState;
import org.assertj.core.util.Lists; 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.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.After;
@@ -36,14 +35,15 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@Slf4j @Slf4j
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
public class TagRootResourceTest { public class TagRootResourceTest extends RepositoryTestBase {
public static final String TAG_PATH = "space/repo/tags/"; public static final String TAG_PATH = "space/repo/tags/";
public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_PATH; public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_PATH;
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); private Dispatcher dispatcher ;
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@@ -56,7 +56,6 @@ public class TagRootResourceTest {
@Mock @Mock
private TagsCommandBuilder tagsCommandBuilder; private TagsCommandBuilder tagsCommandBuilder;
private TagCollectionToDtoMapper tagCollectionToDtoMapper; private TagCollectionToDtoMapper tagCollectionToDtoMapper;
@InjectMocks @InjectMocks
@@ -73,10 +72,8 @@ public class TagRootResourceTest {
public void prepareEnvironment() throws Exception { public void prepareEnvironment() throws Exception {
tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper); tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper);
tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper); tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(Providers super.tagRootResource = Providers.of(tagRootResource);
.of(new RepositoryResource(null, null, null, Providers.of(tagRootResource), null, dispatcher = createDispatcher(getRepositoryRootResource());
null, null, null, null, null, null)), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));