diff --git a/scm-core/src/main/java/sonia/scm/repository/Feature.java b/scm-core/src/main/java/sonia/scm/repository/Feature.java index 1db351267d..1bcaef4de5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Feature.java +++ b/scm-core/src/main/java/sonia/scm/repository/Feature.java @@ -45,5 +45,10 @@ public enum Feature * The default branch of the repository is a combined branch of all * repository branches. */ - COMBINED_DEFAULT_BRANCH + COMBINED_DEFAULT_BRANCH, + /** + * The repository supports computation of incoming changes (either diff or list of changesets) of one branch + * in respect to another target branch. + */ + INCOMING_REVISION } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java index 7217d0e97a..32b633a67c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java @@ -38,6 +38,8 @@ package sonia.scm.repository.api; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.NotSupportedFeatureException; +import sonia.scm.repository.Feature; import sonia.scm.repository.spi.DiffCommand; import sonia.scm.repository.spi.DiffCommandRequest; import sonia.scm.util.IOUtil; @@ -45,6 +47,7 @@ import sonia.scm.util.IOUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -85,10 +88,12 @@ public final class DiffCommandBuilder * only be called from the {@link RepositoryService}. * * @param diffCommand implementation of {@link DiffCommand} + * @param supportedFeatures The supported features of the provider */ - DiffCommandBuilder(DiffCommand diffCommand) + DiffCommandBuilder(DiffCommand diffCommand, Set supportedFeatures) { this.diffCommand = diffCommand; + this.supportedFeatures = supportedFeatures; } //~--- methods -------------------------------------------------------------- @@ -174,7 +179,8 @@ public final class DiffCommandBuilder } /** - * Show the difference only for the given revision. + * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this + * and another revision. * * * @param revision revision for difference @@ -187,6 +193,22 @@ public final class DiffCommandBuilder return this; } + /** + * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given + * here. In other words: What changes would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ + public DiffCommandBuilder setAncestorChangeset(String revision) + { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new NotSupportedFeatureException(Feature.INCOMING_REVISION.name()); + } + request.setAncestorChangeset(revision); + + return this; + } //~--- get methods ---------------------------------------------------------- @@ -215,6 +237,7 @@ public final class DiffCommandBuilder /** implementation of the diff command */ private final DiffCommand diffCommand; + private Set supportedFeatures; /** request for the diff command implementation */ private final DiffCommandRequest request = new DiffCommandRequest(); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java index 73062a0244..7b8e172661 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java @@ -39,10 +39,12 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.NotSupportedFeatureException; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.Feature; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKey; @@ -51,6 +53,7 @@ import sonia.scm.repository.spi.LogCommandRequest; import java.io.IOException; import java.io.Serializable; +import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -104,19 +107,20 @@ public final class LogCommandBuilder /** * Constructs a new {@link LogCommandBuilder}, this constructor should * only be called from the {@link RepositoryService}. - * - * @param cacheManager cache manager + * @param cacheManager cache manager * @param logCommand implementation of the {@link LogCommand} * @param repository repository to query * @param preProcessorUtil + * @param supportedFeatures The supported features of the provider */ LogCommandBuilder(CacheManager cacheManager, LogCommand logCommand, - Repository repository, PreProcessorUtil preProcessorUtil) + Repository repository, PreProcessorUtil preProcessorUtil, Set supportedFeatures) { this.cache = cacheManager.getCache(CACHE_NAME); this.logCommand = logCommand; this.repository = repository; this.preProcessorUtil = preProcessorUtil; + this.supportedFeatures = supportedFeatures; } //~--- methods -------------------------------------------------------------- @@ -397,7 +401,17 @@ public final class LogCommandBuilder return this; } + /** + * Compute the incoming changes of the branch set with {@link #setBranch(String)} in respect to the changeset given + * here. In other words: What changesets would be new to the ancestor changeset given here when the branch would + * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}! + * + * @return {@code this} + */ public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) { + if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) { + throw new NotSupportedFeatureException(Feature.INCOMING_REVISION.name()); + } request.setAncestorChangeset(ancestorChangeset); return this; } @@ -527,6 +541,7 @@ public final class LogCommandBuilder /** Field description */ private final PreProcessorUtil preProcessorUtil; + private Set supportedFeatures; /** repository to query */ private final Repository repository; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index c76fe524fd..ad53c3a8f7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -221,7 +221,7 @@ public final class RepositoryService implements Closeable { logger.debug("create diff command for repository {}", repository.getNamespaceAndName()); - return new DiffCommandBuilder(provider.getDiffCommand()); + return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures()); } /** @@ -253,7 +253,7 @@ public final class RepositoryService implements Closeable { repository.getNamespaceAndName()); return new LogCommandBuilder(cacheManager, provider.getLogCommand(), - repository, preProcessorUtil); + repository, preProcessorUtil, provider.getSupportedFeatures()); } /** diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 4d83d14d5d..95225c9e30 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -43,7 +43,6 @@ import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; -import sonia.scm.io.FileSystem; import sonia.scm.plugin.Extension; import sonia.scm.repository.spi.GitRepositoryServiceProvider; import sonia.scm.schedule.Scheduler; @@ -97,7 +96,7 @@ public class GitRepositoryHandler private final GitWorkdirFactory workdirFactory; private Task task; - + //~--- constructors --------------------------------------------------------- @Inject @@ -126,7 +125,7 @@ public class GitRepositoryHandler scheduleGc(config.getGcExpression()); super.setConfig(config); } - + private void scheduleGc(String expression) { synchronized (LOCK){ @@ -142,7 +141,7 @@ public class GitRepositoryHandler } } } - + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index ae1af333bc..936962eaba 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -34,11 +34,13 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; +import sonia.scm.repository.Feature; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; import java.io.IOException; +import java.util.EnumSet; import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -66,6 +68,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.PULL, Command.MERGE ); + protected static final Set FEATURES = EnumSet.of(Feature.INCOMING_REVISION); //J+ //~--- constructors --------------------------------------------------------- @@ -246,6 +249,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitMergeCommand(context, repository, handler.getWorkdirFactory()); } + @Override + public Set getSupportedFeatures() { + return FEATURES; + } //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index a0d73ad24e..658abbded8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -128,49 +128,6 @@ public class BranchRootResource { } } - @Path("{branch}/diffchangesets/{otherBranchName}") - @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 changeset"), - @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces(VndMediaType.CHANGESET_COLLECTION) - @TypeHint(CollectionDto.class) - public Response changesetDiff(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("branch") String branchName, - @PathParam("otherBranchName") String otherBranchName, - @DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception { - try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { - List allBranches = repositoryService.getBranchesCommand().getBranches().getBranches(); - if (allBranches.stream().noneMatch(branch -> branchName.equals(branch.getName()))) { - throw new NotFoundException("branch", branchName); - } - if (allBranches.stream().noneMatch(branch -> otherBranchName.equals(branch.getName()))) { - throw new NotFoundException("branch", otherBranchName); - } - Repository repository = repositoryService.getRepository(); - RepositoryPermissions.read(repository).check(); - ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService) - .page(page) - .pageSize(pageSize) - .create() - .setBranch(branchName) - .setAncestorChangeset(otherBranchName) - .getChangesets(); - if (changesets != null && changesets.getChangesets() != null) { - PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); - return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build(); - } else { - return Response.ok().build(); - } - } - } - /** * Returns the branches for a repository. * diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index 8167e28f9a..7ab3ef25a8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java @@ -28,7 +28,6 @@ public abstract class BranchToBranchDtoMapper { Links.Builder linksBuilder = linkingTo() .self(resourceLinks.branch().self(namespaceAndName, target.getName())) .single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build()) - .single(linkBuilder("changesetDiff", resourceLinks.branch().changesetDiff(namespaceAndName, target.getName())).build()) .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()) .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build()); target.add(linksBuilder.build()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java index e236b54005..4d15b773cd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffRootResource.java @@ -25,7 +25,7 @@ public class DiffRootResource { public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; - private static final String DIFF_FORMAT_VALUES_REGEX = "NATIVE|GIT|UNIFIED"; + static final String DIFF_FORMAT_VALUES_REGEX = "NATIVE|GIT|UNIFIED"; private final RepositoryServiceFactory serviceFactory; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingChangesetCollectionToDtoMapper.java new file mode 100644 index 0000000000..a2aaabb1a2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingChangesetCollectionToDtoMapper.java @@ -0,0 +1,29 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +public class IncomingChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapper { + + + private final ResourceLinks resourceLinks; + + @Inject + public IncomingChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { + super(changesetToChangesetDtoMapper, resourceLinks); + this.resourceLinks = resourceLinks; + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String source, String target) { + return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, source, target)); + } + + private String createSelfLink(Repository repository, String source, String target) { + return resourceLinks.incoming().changesets(repository.getNamespace(), repository.getName(), source, target); + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java new file mode 100644 index 0000000000..4c43485abd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IncomingRootResource.java @@ -0,0 +1,153 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.api.DiffFormat; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.VndMediaType; + +import javax.validation.constraints.Pattern; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; + +import static sonia.scm.api.v2.resources.DiffRootResource.DIFF_FORMAT_VALUES_REGEX; +import static sonia.scm.api.v2.resources.DiffRootResource.HEADER_CONTENT_DISPOSITION; + +public class IncomingRootResource { + + + private final RepositoryServiceFactory serviceFactory; + + private final IncomingChangesetCollectionToDtoMapper mapper; + + + @Inject + public IncomingRootResource(RepositoryServiceFactory serviceFactory, IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper) { + this.serviceFactory = serviceFactory; + this.mapper = incomingChangesetCollectionToDtoMapper; + } + + /** + * Get the incoming changesets from source to target + *

+ * Example: + *

+ * - master + * - | + * - _______________ ° m1 + * - e | + * - | ° m2 + * - ° e1 | + * - ______|_______ | + * - | | b + * - f a | + * - | | ° b1 + * - ° f1 ° a1 | + * - ° b2 + * - + *

+ * - /incoming/a/master/changesets -> a1 , e1 + * - /incoming/b/master/changesets -> b1 , b2 + * - /incoming/b/f/changesets -> b1 , b2, m2 + * - /incoming/f/b/changesets -> f1 , e1 + * - /incoming/a/b/changesets -> a1 , e1 + * - /incoming/a/b/changesets -> a1 , e1 + * + * @param namespace + * @param name + * @param source can be a changeset id or a branch name + * @param target can be a changeset id or a branch name + * @param page + * @param pageSize + * @return + * @throws Exception + */ + @Path("{source}/{target}/changesets") + @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 changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response incomingChangesets(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("source") String source, + @PathParam("target") String target, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + ChangesetPagingResult changesets = new PagedLogCommandBuilder(repositoryService) + .page(page) + .pageSize(pageSize) + .create() + .setStartChangeset(source) + .setAncestorChangeset(target) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(mapper.map(page, pageSize, pageResult, repository, source, target)).build(); + } else { + return Response.ok().build(); + } + } + } + + + @Path("{source}/{target}/diff") + @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 changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.DIFF) + @TypeHint(CollectionDto.class) + public Response incomingDiff(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("source") String source, + @PathParam("target") String target, + @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format) throws IOException { + + + HttpUtil.checkForCRLFInjection(source); + HttpUtil.checkForCRLFInjection(target); + DiffFormat diffFormat = DiffFormat.valueOf(format); + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + StreamingOutput responseEntry = output -> + repositoryService.getDiffCommand() + .setRevision(source) + .setAncestorChangeset(target) + .setFormat(diffFormat) + .retrieveContent(output); + + return Response.ok(responseEntry) + .header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, source))) + .build(); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c4f76d0167..b884a37771 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -44,6 +44,7 @@ public class RepositoryResource { private final Provider modificationsRootResource; private final Provider fileHistoryRootResource; private final Provider mergeResource; + private final Provider incomingRootResource; @Inject public RepositoryResource( @@ -57,6 +58,7 @@ public class RepositoryResource { Provider diffRootResource, Provider modificationsRootResource, Provider fileHistoryRootResource, + Provider incomingRootResource, Provider mergeResource) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; @@ -72,6 +74,7 @@ public class RepositoryResource { this.modificationsRootResource = modificationsRootResource; this.fileHistoryRootResource = fileHistoryRootResource; this.mergeResource = mergeResource; + this.incomingRootResource = incomingRootResource; } /** @@ -196,7 +199,14 @@ public class RepositoryResource { } @Path("modifications/") - public ModificationsRootResource modifications() {return modificationsRootResource.get(); } + public ModificationsRootResource modifications() { + return modificationsRootResource.get(); + } + + @Path("incoming/") + public IncomingRootResource incoming() { + return incomingRootResource.get(); + } @Path("merge/") public MergeResource merge() {return mergeResource.get(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index fa1522bc37..30ccb79735 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -6,6 +6,7 @@ import de.otto.edison.hal.Links; import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; +import sonia.scm.repository.Feature; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; @@ -55,6 +56,10 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(0)).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(10)).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setAncestorChangeset(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(INCOMING_CHANGESETS_URL + "src_changeset_id/target_changeset_id/changesets") + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + + @Test + public void shouldGetSinglePageOfIncomingChangesets() throws Exception { + String id = "revision_123"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(20)).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(10)).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setAncestorChangeset(anyString())).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(INCOMING_CHANGESETS_URL + "src_changeset_id/target_changeset_id/changesets?page=2") + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(200, response.getStatus()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + + @Test + public void shouldGetDiffs() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder); + MockHttpRequest request = MockHttpRequest + .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()) + .isEqualTo(200); + String expectedHeader = "Content-Disposition"; + String expectedValue = "attachment; filename=\"repo-src_changeset_id.diff\"; filename*=utf-8''repo-src_changeset_id.diff"; + assertThat(response.getOutputHeaders().containsKey(expectedHeader)).isTrue(); + assertThat((String) response.getOutputHeaders().get("Content-Disposition").get(0)) + .contains(expectedValue); + } + + @Test + public void shouldGet404OnMissingRepository() throws URISyntaxException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(new NotFoundException("Text", "x")); + MockHttpRequest request = MockHttpRequest + .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet404OnMissingRevision() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); + + MockHttpRequest request = MockHttpRequest + .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet400OnCrlfInjection() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x")); + MockHttpRequest request = MockHttpRequest + .get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + assertThat(response.getContentAsString()).contains("parameter contains an illegal character"); + } + + @Test + public void shouldGet400OnUnknownFormat() throws Exception { + when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder); + when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Test", "test")); + MockHttpRequest request = MockHttpRequest + .get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown") + .accept(VndMediaType.DIFF); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index 0ccb923e45..b2daf0536c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -21,6 +21,7 @@ public abstract class RepositoryTestBase { protected Provider modificationsRootResource; protected Provider fileHistoryRootResource; protected Provider repositoryCollectionResource; + protected Provider incomingRootResource; protected Provider mergeResource; @@ -38,6 +39,7 @@ public abstract class RepositoryTestBase { diffRootResource, modificationsRootResource, fileHistoryRootResource, + incomingRootResource, mergeResource)), repositoryCollectionResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 0343888d6a..435d8b4673 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -20,6 +20,7 @@ public class ResourceLinksMock { when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); + when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo)); when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));