mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 22:15:45 +01:00
merge with 2.0.0-m3
This commit is contained in:
@@ -45,5 +45,10 @@ public enum Feature
|
|||||||
* The default branch of the repository is a combined branch of all
|
* The default branch of the repository is a combined branch of all
|
||||||
* repository branches.
|
* 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ package sonia.scm.repository.api;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.NotSupportedFeatureException;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.spi.DiffCommand;
|
import sonia.scm.repository.spi.DiffCommand;
|
||||||
import sonia.scm.repository.spi.DiffCommandRequest;
|
import sonia.scm.repository.spi.DiffCommandRequest;
|
||||||
import sonia.scm.util.IOUtil;
|
import sonia.scm.util.IOUtil;
|
||||||
@@ -45,6 +47,7 @@ import sonia.scm.util.IOUtil;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -85,10 +88,12 @@ public final class DiffCommandBuilder
|
|||||||
* only be called from the {@link RepositoryService}.
|
* only be called from the {@link RepositoryService}.
|
||||||
*
|
*
|
||||||
* @param diffCommand implementation of {@link DiffCommand}
|
* @param diffCommand implementation of {@link DiffCommand}
|
||||||
|
* @param supportedFeatures The supported features of the provider
|
||||||
*/
|
*/
|
||||||
DiffCommandBuilder(DiffCommand diffCommand)
|
DiffCommandBuilder(DiffCommand diffCommand, Set<Feature> supportedFeatures)
|
||||||
{
|
{
|
||||||
this.diffCommand = diffCommand;
|
this.diffCommand = diffCommand;
|
||||||
|
this.supportedFeatures = supportedFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- 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
|
* @param revision revision for difference
|
||||||
@@ -187,6 +193,22 @@ public final class DiffCommandBuilder
|
|||||||
|
|
||||||
return this;
|
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 ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
@@ -215,6 +237,7 @@ public final class DiffCommandBuilder
|
|||||||
|
|
||||||
/** implementation of the diff command */
|
/** implementation of the diff command */
|
||||||
private final DiffCommand diffCommand;
|
private final DiffCommand diffCommand;
|
||||||
|
private Set<Feature> supportedFeatures;
|
||||||
|
|
||||||
/** request for the diff command implementation */
|
/** request for the diff command implementation */
|
||||||
private final DiffCommandRequest request = new DiffCommandRequest();
|
private final DiffCommandRequest request = new DiffCommandRequest();
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ import com.google.common.base.Objects;
|
|||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.NotSupportedFeatureException;
|
||||||
import sonia.scm.cache.Cache;
|
import sonia.scm.cache.Cache;
|
||||||
import sonia.scm.cache.CacheManager;
|
import sonia.scm.cache.CacheManager;
|
||||||
import sonia.scm.repository.Changeset;
|
import sonia.scm.repository.Changeset;
|
||||||
import sonia.scm.repository.ChangesetPagingResult;
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.PreProcessorUtil;
|
import sonia.scm.repository.PreProcessorUtil;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryCacheKey;
|
import sonia.scm.repository.RepositoryCacheKey;
|
||||||
@@ -51,6 +53,7 @@ import sonia.scm.repository.spi.LogCommandRequest;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -104,19 +107,20 @@ public final class LogCommandBuilder
|
|||||||
/**
|
/**
|
||||||
* Constructs a new {@link LogCommandBuilder}, this constructor should
|
* Constructs a new {@link LogCommandBuilder}, this constructor should
|
||||||
* only be called from the {@link RepositoryService}.
|
* only be called from the {@link RepositoryService}.
|
||||||
*
|
* @param cacheManager cache manager
|
||||||
* @param cacheManager cache manager
|
|
||||||
* @param logCommand implementation of the {@link LogCommand}
|
* @param logCommand implementation of the {@link LogCommand}
|
||||||
* @param repository repository to query
|
* @param repository repository to query
|
||||||
* @param preProcessorUtil
|
* @param preProcessorUtil
|
||||||
|
* @param supportedFeatures The supported features of the provider
|
||||||
*/
|
*/
|
||||||
LogCommandBuilder(CacheManager cacheManager, LogCommand logCommand,
|
LogCommandBuilder(CacheManager cacheManager, LogCommand logCommand,
|
||||||
Repository repository, PreProcessorUtil preProcessorUtil)
|
Repository repository, PreProcessorUtil preProcessorUtil, Set<Feature> supportedFeatures)
|
||||||
{
|
{
|
||||||
this.cache = cacheManager.getCache(CACHE_NAME);
|
this.cache = cacheManager.getCache(CACHE_NAME);
|
||||||
this.logCommand = logCommand;
|
this.logCommand = logCommand;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.preProcessorUtil = preProcessorUtil;
|
this.preProcessorUtil = preProcessorUtil;
|
||||||
|
this.supportedFeatures = supportedFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
@@ -397,7 +401,17 @@ public final class LogCommandBuilder
|
|||||||
return this;
|
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) {
|
public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) {
|
||||||
|
if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
|
||||||
|
throw new NotSupportedFeatureException(Feature.INCOMING_REVISION.name());
|
||||||
|
}
|
||||||
request.setAncestorChangeset(ancestorChangeset);
|
request.setAncestorChangeset(ancestorChangeset);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -527,6 +541,7 @@ public final class LogCommandBuilder
|
|||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private final PreProcessorUtil preProcessorUtil;
|
private final PreProcessorUtil preProcessorUtil;
|
||||||
|
private Set<Feature> supportedFeatures;
|
||||||
|
|
||||||
/** repository to query */
|
/** repository to query */
|
||||||
private final Repository repository;
|
private final Repository repository;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import sonia.scm.repository.spi.MergeCommandRequest;
|
|||||||
*
|
*
|
||||||
* To actually merge <code>feature_branch</code> into <code>integration_branch</code> do this:
|
* To actually merge <code>feature_branch</code> into <code>integration_branch</code> do this:
|
||||||
* <pre><code>
|
* <pre><code>
|
||||||
* repositoryService.gerMergeCommand()
|
* repositoryService.getMergeCommand()
|
||||||
* .setBranchToMerge("feature_branch")
|
* .setBranchToMerge("feature_branch")
|
||||||
* .setTargetBranch("integration_branch")
|
* .setTargetBranch("integration_branch")
|
||||||
* .executeMerge();
|
* .executeMerge();
|
||||||
@@ -33,7 +33,7 @@ import sonia.scm.repository.spi.MergeCommandRequest;
|
|||||||
*
|
*
|
||||||
* To check whether they can be merged without conflicts beforehand do this:
|
* To check whether they can be merged without conflicts beforehand do this:
|
||||||
* <pre><code>
|
* <pre><code>
|
||||||
* repositoryService.gerMergeCommand()
|
* repositoryService.getMergeCommand()
|
||||||
* .setBranchToMerge("feature_branch")
|
* .setBranchToMerge("feature_branch")
|
||||||
* .setTargetBranch("integration_branch")
|
* .setTargetBranch("integration_branch")
|
||||||
* .dryRun()
|
* .dryRun()
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ public final class RepositoryService implements Closeable {
|
|||||||
logger.debug("create diff command for repository {}",
|
logger.debug("create diff command for repository {}",
|
||||||
repository.getNamespaceAndName());
|
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());
|
repository.getNamespaceAndName());
|
||||||
|
|
||||||
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
|
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
|
||||||
repository, preProcessorUtil);
|
repository, preProcessorUtil, provider.getSupportedFeatures());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,8 +363,8 @@ public final class RepositoryService implements Closeable {
|
|||||||
* by the implementation of the repository service provider.
|
* by the implementation of the repository service provider.
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public MergeCommandBuilder gerMergeCommand() {
|
public MergeCommandBuilder getMergeCommand() {
|
||||||
logger.debug("create unbundle command for repository {}",
|
logger.debug("create merge command for repository {}",
|
||||||
repository.getNamespaceAndName());
|
repository.getNamespaceAndName());
|
||||||
|
|
||||||
return new MergeCommandBuilder(provider.getMergeCommand());
|
return new MergeCommandBuilder(provider.getMergeCommand());
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ public class VndMediaType {
|
|||||||
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
||||||
@SuppressWarnings("squid:S2068")
|
@SuppressWarnings("squid:S2068")
|
||||||
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
|
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
|
||||||
|
public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
|
||||||
|
public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;
|
||||||
|
|
||||||
public static final String ME = PREFIX + "me" + SUFFIX;
|
public static final String ME = PREFIX + "me" + SUFFIX;
|
||||||
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import org.eclipse.jgit.lib.RepositoryCache;
|
|||||||
|
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
import sonia.scm.repository.spi.HookEventFacade;
|
import sonia.scm.repository.spi.HookEventFacade;
|
||||||
|
import sonia.scm.web.CollectingPackParserListener;
|
||||||
import sonia.scm.web.GitReceiveHook;
|
import sonia.scm.web.GitReceiveHook;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
@@ -64,10 +65,10 @@ public class ScmTransportProtocol extends TransportProtocol
|
|||||||
{
|
{
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private static final String NAME = "scm";
|
public static final String NAME = "scm";
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private static final Set<String> SCHEMES = ImmutableSet.of("scm");
|
private static final Set<String> SCHEMES = ImmutableSet.of(NAME);
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -234,6 +235,8 @@ public class ScmTransportProtocol extends TransportProtocol
|
|||||||
|
|
||||||
pack.setPreReceiveHook(hook);
|
pack.setPreReceiveHook(hook);
|
||||||
pack.setPostReceiveHook(hook);
|
pack.setPostReceiveHook(hook);
|
||||||
|
|
||||||
|
CollectingPackParserListener.set(pack);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pack;
|
return pack;
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.io.FileSystem;
|
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
import sonia.scm.repository.spi.GitRepositoryServiceProvider;
|
import sonia.scm.repository.spi.GitRepositoryServiceProvider;
|
||||||
import sonia.scm.schedule.Scheduler;
|
import sonia.scm.schedule.Scheduler;
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import com.google.common.base.Strings;
|
|||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
|
||||||
import org.eclipse.jgit.api.MergeResult;
|
import org.eclipse.jgit.api.MergeResult;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.merge.MergeStrategy;
|
import org.eclipse.jgit.merge.MergeStrategy;
|
||||||
@@ -15,6 +17,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import sonia.scm.repository.GitWorkdirFactory;
|
import sonia.scm.repository.GitWorkdirFactory;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.repository.Person;
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
import sonia.scm.repository.api.MergeCommandResult;
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
@@ -22,6 +25,9 @@ import sonia.scm.user.User;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
|
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
|
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
|
||||||
@@ -40,6 +46,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MergeCommandResult merge(MergeCommandRequest request) {
|
public MergeCommandResult merge(MergeCommandRequest request) {
|
||||||
|
RepositoryPermissions.push(context.getRepository().getId()).check();
|
||||||
|
|
||||||
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
|
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||||
Repository repository = workingCopy.get();
|
Repository repository = workingCopy.get();
|
||||||
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
||||||
@@ -88,20 +96,43 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkOutTargetBranch() {
|
private void checkOutTargetBranch() throws IOException {
|
||||||
try {
|
try {
|
||||||
clone.checkout().setName(target).call();
|
clone.checkout().setName(target).call();
|
||||||
|
} catch (RefNotFoundException e) {
|
||||||
|
logger.trace("could not checkout target branch {} for merge directly; trying to create local branch", target, e);
|
||||||
|
checkOutTargetAsNewLocalBranch();
|
||||||
} catch (GitAPIException e) {
|
} catch (GitAPIException e) {
|
||||||
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e);
|
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkOutTargetAsNewLocalBranch() throws IOException {
|
||||||
|
try {
|
||||||
|
ObjectId targetRevision = resolveRevision(target);
|
||||||
|
if (targetRevision == null) {
|
||||||
|
throw notFound(entity("revision", target).in(context.getRepository()));
|
||||||
|
}
|
||||||
|
clone.checkout().setStartPoint(targetRevision.getName()).setName(target).setCreateBranch(true).call();
|
||||||
|
} catch (RefNotFoundException e) {
|
||||||
|
logger.debug("could not checkout target branch {} for merge as local branch", target, e);
|
||||||
|
throw notFound(entity("revision", target).in(context.getRepository()));
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge as local branch: " + target, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MergeResult doMergeInClone() throws IOException {
|
private MergeResult doMergeInClone() throws IOException {
|
||||||
MergeResult result;
|
MergeResult result;
|
||||||
try {
|
try {
|
||||||
|
ObjectId sourceRevision = resolveRevision(toMerge);
|
||||||
|
if (sourceRevision == null) {
|
||||||
|
throw notFound(entity("revision", toMerge).in(context.getRepository()));
|
||||||
|
}
|
||||||
result = clone.merge()
|
result = clone.merge()
|
||||||
|
.setFastForward(FastForwardMode.NO_FF)
|
||||||
.setCommit(false) // we want to set the author manually
|
.setCommit(false) // we want to set the author manually
|
||||||
.include(toMerge, resolveRevision(toMerge))
|
.include(toMerge, sourceRevision)
|
||||||
.call();
|
.call();
|
||||||
} catch (GitAPIException e) {
|
} catch (GitAPIException e) {
|
||||||
throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e);
|
throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e);
|
||||||
@@ -113,10 +144,12 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
|||||||
logger.debug("merged branch {} into {}", toMerge, target);
|
logger.debug("merged branch {} into {}", toMerge, target);
|
||||||
Person authorToUse = determineAuthor();
|
Person authorToUse = determineAuthor();
|
||||||
try {
|
try {
|
||||||
clone.commit()
|
if (!clone.status().call().isClean()) {
|
||||||
.setAuthor(authorToUse.getName(), authorToUse.getMail())
|
clone.commit()
|
||||||
.setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
|
.setAuthor(authorToUse.getName(), authorToUse.getMail())
|
||||||
.call();
|
.setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
|
||||||
|
.call();
|
||||||
|
}
|
||||||
} catch (GitAPIException e) {
|
} catch (GitAPIException e) {
|
||||||
throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e);
|
throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e);
|
||||||
}
|
}
|
||||||
@@ -147,7 +180,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
|||||||
try {
|
try {
|
||||||
clone.push().call();
|
clone.push().call();
|
||||||
} catch (GitAPIException e) {
|
} catch (GitAPIException e) {
|
||||||
throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + toMerge + " to origin", e);
|
throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + target + " to origin", e);
|
||||||
}
|
}
|
||||||
logger.debug("pushed merged branch {}", target);
|
logger.debug("pushed merged branch {}", target);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,13 @@
|
|||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.GitRepositoryHandler;
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.api.Command;
|
import sonia.scm.repository.api.Command;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
@@ -66,6 +68,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
Command.PULL,
|
Command.PULL,
|
||||||
Command.MERGE
|
Command.MERGE
|
||||||
);
|
);
|
||||||
|
protected static final Set<Feature> FEATURES = EnumSet.of(Feature.INCOMING_REVISION);
|
||||||
//J+
|
//J+
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
@@ -246,6 +249,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
|
return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Feature> getSupportedFeatures() {
|
||||||
|
return FEATURES;
|
||||||
|
}
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package sonia.scm.repository.spi;
|
|||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||||
import org.eclipse.jgit.util.FileUtils;
|
import org.eclipse.jgit.util.FileUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -45,12 +46,16 @@ public class SimpleGitWorkdirFactory implements GitWorkdirFactory {
|
|||||||
|
|
||||||
protected Repository cloneRepository(File bareRepository, File target) throws GitAPIException {
|
protected Repository cloneRepository(File bareRepository, File target) throws GitAPIException {
|
||||||
return Git.cloneRepository()
|
return Git.cloneRepository()
|
||||||
.setURI(bareRepository.getAbsolutePath())
|
.setURI(createScmTransportProtocolUri(bareRepository))
|
||||||
.setDirectory(target)
|
.setDirectory(target)
|
||||||
.call()
|
.call()
|
||||||
.getRepository();
|
.getRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String createScmTransportProtocolUri(File bareRepository) {
|
||||||
|
return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
private void close(Repository repository) {
|
private void close(Repository repository) {
|
||||||
repository.close();
|
repository.close();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,20 +6,33 @@ import org.apache.shiro.subject.SimplePrincipalCollection;
|
|||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.lib.PersonIdent;
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||||
|
import org.eclipse.jgit.transport.Transport;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
import sonia.scm.repository.Person;
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.repository.PreProcessorUtil;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.repository.api.HookContextFactory;
|
||||||
import sonia.scm.repository.api.MergeCommandResult;
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static com.google.inject.util.Providers.of;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
||||||
public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
private static final String REALM = "AdminRealm";
|
private static final String REALM = "AdminRealm";
|
||||||
@@ -27,6 +40,27 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
private ScmTransportProtocol scmTransportProtocol;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindScmProtocol() {
|
||||||
|
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
|
||||||
|
RepositoryManager repositoryManager = mock(RepositoryManager.class);
|
||||||
|
HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory);
|
||||||
|
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
|
||||||
|
scmTransportProtocol = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler));
|
||||||
|
|
||||||
|
Transport.register(scmTransportProtocol);
|
||||||
|
|
||||||
|
when(gitRepositoryHandler.getRepositoryId(any())).thenReturn("1");
|
||||||
|
when(repositoryManager.get("1")).thenReturn(new sonia.scm.repository.Repository());
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unregisterScmProtocol() {
|
||||||
|
Transport.unregister(scmTransportProtocol);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldDetectMergeableBranches() {
|
public void shouldDetectMergeableBranches() {
|
||||||
GitMergeCommand command = createCommand();
|
GitMergeCommand command = createCommand();
|
||||||
@@ -77,6 +111,30 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
|||||||
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
|
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotMergeTwice() throws IOException, GitAPIException {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
request.setBranchToMerge("mergeable");
|
||||||
|
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||||
|
|
||||||
|
MergeCommandResult mergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
assertThat(mergeCommandResult.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
Repository repository = createContext().open();
|
||||||
|
ObjectId firstMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId();
|
||||||
|
|
||||||
|
MergeCommandResult secondMergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
assertThat(secondMergeCommandResult.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
ObjectId secondMergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call().iterator().next().getId();
|
||||||
|
|
||||||
|
assertThat(secondMergeCommit).isEqualTo(firstMergeCommit);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException {
|
public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException {
|
||||||
GitMergeCommand command = createCommand();
|
GitMergeCommand command = createCommand();
|
||||||
@@ -111,11 +169,14 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "admin", password = "secret")
|
|
||||||
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
|
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
|
||||||
|
SimplePrincipalCollection principals = new SimplePrincipalCollection();
|
||||||
|
principals.add("admin", REALM);
|
||||||
|
principals.add( new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM);
|
||||||
shiro.setSubject(
|
shiro.setSubject(
|
||||||
new Subject.Builder()
|
new Subject.Builder()
|
||||||
.principals(new SimplePrincipalCollection(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM))
|
.principals(principals)
|
||||||
|
.authenticated(true)
|
||||||
.buildSubject());
|
.buildSubject());
|
||||||
GitMergeCommand command = createCommand();
|
GitMergeCommand command = createCommand();
|
||||||
MergeCommandRequest request = new MergeCommandRequest();
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
@@ -133,6 +194,32 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
|||||||
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
|
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldMergeIntoNotDefaultBranch() throws IOException, GitAPIException {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||||
|
request.setTargetBranch("mergeable");
|
||||||
|
request.setBranchToMerge("master");
|
||||||
|
|
||||||
|
MergeCommandResult mergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
Repository repository = createContext().open();
|
||||||
|
assertThat(mergeCommandResult.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("mergeable")).setMaxCount(1).call();
|
||||||
|
RevCommit mergeCommit = commits.iterator().next();
|
||||||
|
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
|
||||||
|
String message = mergeCommit.getFullMessage();
|
||||||
|
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
|
||||||
|
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
|
||||||
|
assertThat(message).contains("master", "mergeable");
|
||||||
|
// We expect the merge result of file b.txt here by looking up the sha hash of its content.
|
||||||
|
// If the file is missing (aka not merged correctly) this will throw a MissingObjectException:
|
||||||
|
byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes();
|
||||||
|
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
|
||||||
|
}
|
||||||
|
|
||||||
private GitMergeCommand createCommand() {
|
private GitMergeCommand createCommand() {
|
||||||
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
|
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,23 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||||
|
import org.eclipse.jgit.transport.Transport;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import sonia.scm.repository.GitRepositoryHandler;
|
||||||
|
import sonia.scm.repository.PreProcessorUtil;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
|
import sonia.scm.repository.api.HookContextFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static com.google.inject.util.Providers.of;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
@@ -18,6 +27,14 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
|||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindScmProtocol() {
|
||||||
|
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
|
||||||
|
HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory);
|
||||||
|
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
|
||||||
|
Transport.register(new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler)));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
|
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
|
||||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import {contextPath} from "./urls";
|
import {contextPath} from "./urls";
|
||||||
|
|
||||||
export const NOT_FOUND_ERROR_MESSAGE = "not found";
|
export const NOT_FOUND_ERROR = new Error("not found");
|
||||||
export const UNAUTHORIZED_ERROR_MESSAGE = "unauthorized";
|
export const UNAUTHORIZED_ERROR = new Error("unauthorized");
|
||||||
|
export const CONFLICT_ERROR = new Error("conflict");
|
||||||
|
|
||||||
const fetchOptions: RequestOptions = {
|
const fetchOptions: RequestOptions = {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
@@ -15,24 +16,26 @@ function handleStatusCode(response: Response) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
return throwErrorWithMessage(response, UNAUTHORIZED_ERROR_MESSAGE);
|
return throwError(response, UNAUTHORIZED_ERROR);
|
||||||
case 404:
|
case 404:
|
||||||
return throwErrorWithMessage(response, NOT_FOUND_ERROR_MESSAGE);
|
return throwError(response, NOT_FOUND_ERROR);
|
||||||
|
case 409:
|
||||||
|
return throwError(response, CONFLICT_ERROR);
|
||||||
default:
|
default:
|
||||||
return throwErrorWithMessage(response, "server returned status code " + response.status);
|
return throwError(response, new Error("server returned status code " + response.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function throwErrorWithMessage(response: Response, message: string) {
|
function throwError(response: Response, err: Error) {
|
||||||
return response.json().then(
|
return response.json().then(
|
||||||
json => {
|
json => {
|
||||||
throw Error(json.message);
|
throw Error(json.message);
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
throw Error(message);
|
throw err;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export { default as Tooltip } from "./Tooltip";
|
|||||||
export { getPageFromMatch } from "./urls";
|
export { getPageFromMatch } from "./urls";
|
||||||
export { default as Autocomplete} from "./Autocomplete";
|
export { default as Autocomplete} from "./Autocomplete";
|
||||||
|
|
||||||
export { apiClient, NOT_FOUND_ERROR_MESSAGE, UNAUTHORIZED_ERROR_MESSAGE } from "./apiclient.js";
|
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR, CONFLICT_ERROR } from "./apiclient.js";
|
||||||
|
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
export * from "./buttons";
|
export * from "./buttons";
|
||||||
|
|||||||
@@ -2,10 +2,7 @@
|
|||||||
import type { Me } from "@scm-manager/ui-types";
|
import type { Me } from "@scm-manager/ui-types";
|
||||||
import * as types from "./types";
|
import * as types from "./types";
|
||||||
|
|
||||||
import {
|
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
||||||
apiClient,
|
|
||||||
UNAUTHORIZED_ERROR_MESSAGE
|
|
||||||
} from "@scm-manager/ui-components";
|
|
||||||
import { isPending } from "./pending";
|
import { isPending } from "./pending";
|
||||||
import { getFailure } from "./failure";
|
import { getFailure } from "./failure";
|
||||||
import {
|
import {
|
||||||
@@ -190,7 +187,7 @@ export const fetchMe = (link: string) => {
|
|||||||
dispatch(fetchMeSuccess(me));
|
dispatch(fetchMeSuccess(me));
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
if (error.message === UNAUTHORIZED_ERROR_MESSAGE) {
|
if (error === UNAUTHORIZED_ERROR) {
|
||||||
dispatch(fetchMeUnauthenticated());
|
dispatch(fetchMeUnauthenticated());
|
||||||
} else {
|
} else {
|
||||||
dispatch(fetchMeFailure(error));
|
dispatch(fetchMeFailure(error));
|
||||||
|
|||||||
@@ -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<Branch> 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<Changeset> 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.
|
* Returns the branches for a repository.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ public abstract class BranchToBranchDtoMapper {
|
|||||||
Links.Builder linksBuilder = linkingTo()
|
Links.Builder linksBuilder = linkingTo()
|
||||||
.self(resourceLinks.branch().self(namespaceAndName, target.getName()))
|
.self(resourceLinks.branch().self(namespaceAndName, target.getName()))
|
||||||
.single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, target.getName())).build())
|
.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("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build())
|
||||||
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
|
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision())).build());
|
||||||
target.add(linksBuilder.build());
|
target.add(linksBuilder.build());
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class DiffRootResource {
|
|||||||
|
|
||||||
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
|
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;
|
private final RepositoryServiceFactory serviceFactory;
|
||||||
|
|
||||||
|
|||||||
@@ -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<Changeset> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 <code>source</code> to <code>target</code>
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
* <p>
|
||||||
|
* - master
|
||||||
|
* - |
|
||||||
|
* - _______________ ° m1
|
||||||
|
* - e |
|
||||||
|
* - | ° m2
|
||||||
|
* - ° e1 |
|
||||||
|
* - ______|_______ |
|
||||||
|
* - | | b
|
||||||
|
* - f a |
|
||||||
|
* - | | ° b1
|
||||||
|
* - ° f1 ° a1 |
|
||||||
|
* - ° b2
|
||||||
|
* -
|
||||||
|
* <p>
|
||||||
|
* - /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<Changeset> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,8 @@ public class MapperModule extends AbstractModule {
|
|||||||
bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass());
|
bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass());
|
||||||
bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass());
|
bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass());
|
||||||
|
|
||||||
|
bind(MergeResultToDtoMapper.class).to(Mappers.getMapper(MergeResultToDtoMapper.class).getClass());
|
||||||
|
|
||||||
// no mapstruct required
|
// no mapstruct required
|
||||||
bind(UIPluginDtoMapper.class);
|
bind(UIPluginDtoMapper.class);
|
||||||
bind(UIPluginDtoCollectionMapper.class);
|
bind(UIPluginDtoCollectionMapper.class);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
public class MergeCommandDto {
|
||||||
|
|
||||||
|
@NotEmpty
|
||||||
|
private String sourceRevision;
|
||||||
|
@NotEmpty
|
||||||
|
private String targetRevision;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.http.HttpStatus;
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.api.MergeCommandBuilder;
|
||||||
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
|
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||||
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class MergeResource {
|
||||||
|
|
||||||
|
private final RepositoryServiceFactory serviceFactory;
|
||||||
|
private final MergeResultToDtoMapper mapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MergeResource(RepositoryServiceFactory serviceFactory, MergeResultToDtoMapper mapper) {
|
||||||
|
this.serviceFactory = serviceFactory;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("")
|
||||||
|
@Produces(VndMediaType.MERGE_RESULT)
|
||||||
|
@Consumes(VndMediaType.MERGE_COMMAND)
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 204, condition = "merge has been executed successfully"),
|
||||||
|
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||||
|
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the privilege to write the repository"),
|
||||||
|
@ResponseCode(code = 409, condition = "The branches could not be merged automatically due to conflicts (conflicting files will be returned)"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response merge(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
|
||||||
|
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||||
|
log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
|
||||||
|
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||||
|
MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge();
|
||||||
|
if (mergeCommandResult.isSuccess()) {
|
||||||
|
return Response.noContent().build();
|
||||||
|
} else {
|
||||||
|
return Response.status(HttpStatus.SC_CONFLICT).entity(mapper.map(mergeCommandResult)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("dry-run/")
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 204, condition = "merge can be done automatically"),
|
||||||
|
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||||
|
@ResponseCode(code = 409, condition = "The branches can not be merged automatically due to conflicts"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
|
||||||
|
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||||
|
log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
|
||||||
|
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||||
|
MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun();
|
||||||
|
if (mergeCommandResult.isMergeable()) {
|
||||||
|
return Response.noContent().build();
|
||||||
|
} else {
|
||||||
|
return Response.status(HttpStatus.SC_CONFLICT).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MergeCommandBuilder createMergeCommand(MergeCommandDto mergeCommand, RepositoryService repositoryService) {
|
||||||
|
return repositoryService
|
||||||
|
.getMergeCommand()
|
||||||
|
.setBranchToMerge(mergeCommand.getSourceRevision())
|
||||||
|
.setTargetBranch(mergeCommand.getTargetRevision());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class MergeResultDto {
|
||||||
|
private Collection<String> filesWithConflict;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface MergeResultToDtoMapper {
|
||||||
|
MergeResultDto map(MergeCommandResult result);
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import sonia.scm.repository.RepositoryManager;
|
|||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
@@ -44,6 +43,8 @@ public class RepositoryResource {
|
|||||||
private final Provider<DiffRootResource> diffRootResource;
|
private final Provider<DiffRootResource> diffRootResource;
|
||||||
private final Provider<ModificationsRootResource> modificationsRootResource;
|
private final Provider<ModificationsRootResource> modificationsRootResource;
|
||||||
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
|
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||||
|
private final Provider<MergeResource> mergeResource;
|
||||||
|
private final Provider<IncomingRootResource> incomingRootResource;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RepositoryResource(
|
public RepositoryResource(
|
||||||
@@ -56,8 +57,9 @@ public class RepositoryResource {
|
|||||||
Provider<PermissionRootResource> permissionRootResource,
|
Provider<PermissionRootResource> permissionRootResource,
|
||||||
Provider<DiffRootResource> diffRootResource,
|
Provider<DiffRootResource> diffRootResource,
|
||||||
Provider<ModificationsRootResource> modificationsRootResource,
|
Provider<ModificationsRootResource> modificationsRootResource,
|
||||||
Provider<FileHistoryRootResource> fileHistoryRootResource
|
Provider<FileHistoryRootResource> fileHistoryRootResource,
|
||||||
) {
|
Provider<IncomingRootResource> incomingRootResource,
|
||||||
|
Provider<MergeResource> mergeResource) {
|
||||||
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.repositoryToDtoMapper = repositoryToDtoMapper;
|
this.repositoryToDtoMapper = repositoryToDtoMapper;
|
||||||
@@ -71,6 +73,8 @@ public class RepositoryResource {
|
|||||||
this.diffRootResource = diffRootResource;
|
this.diffRootResource = diffRootResource;
|
||||||
this.modificationsRootResource = modificationsRootResource;
|
this.modificationsRootResource = modificationsRootResource;
|
||||||
this.fileHistoryRootResource = fileHistoryRootResource;
|
this.fileHistoryRootResource = fileHistoryRootResource;
|
||||||
|
this.mergeResource = mergeResource;
|
||||||
|
this.incomingRootResource = incomingRootResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,8 +198,18 @@ public class RepositoryResource {
|
|||||||
return permissionRootResource.get();
|
return permissionRootResource.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("modifications/")
|
@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(); }
|
||||||
|
|
||||||
private Optional<Response> handleNotArchived(Throwable throwable) {
|
private Optional<Response> handleNotArchived(Throwable throwable) {
|
||||||
if (throwable instanceof RepositoryIsNotArchivedException) {
|
if (throwable instanceof RepositoryIsNotArchivedException) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import de.otto.edison.hal.Links;
|
|||||||
import org.mapstruct.AfterMapping;
|
import org.mapstruct.AfterMapping;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.MappingTarget;
|
import org.mapstruct.MappingTarget;
|
||||||
|
import sonia.scm.repository.Feature;
|
||||||
import sonia.scm.repository.HealthCheckFailure;
|
import sonia.scm.repository.HealthCheckFailure;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryPermissions;
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
@@ -55,6 +56,14 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
|||||||
if (repositoryService.isSupported(Command.BRANCHES)) {
|
if (repositoryService.isSupported(Command.BRANCHES)) {
|
||||||
linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName())));
|
linksBuilder.single(link("branches", resourceLinks.branchCollection().self(target.getNamespace(), target.getName())));
|
||||||
}
|
}
|
||||||
|
if (repositoryService.isSupported(Feature.INCOMING_REVISION)) {
|
||||||
|
linksBuilder.single(link("incomingChangesets", resourceLinks.incoming().changesets(target.getNamespace(), target.getName())));
|
||||||
|
linksBuilder.single(link("incomingDiff", resourceLinks.incoming().diff(target.getNamespace(), target.getName())));
|
||||||
|
}
|
||||||
|
if (repositoryService.isSupported(Command.MERGE)) {
|
||||||
|
linksBuilder.single(link("merge", resourceLinks.merge().merge(target.getNamespace(), target.getName())));
|
||||||
|
linksBuilder.single(link("mergeDryRun", resourceLinks.merge().dryRun(target.getNamespace(), target.getName())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
linksBuilder.single(link("changesets", resourceLinks.changeset().all(target.getNamespace(), target.getName())));
|
linksBuilder.single(link("changesets", resourceLinks.changeset().all(target.getNamespace(), target.getName())));
|
||||||
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName())));
|
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(target.getNamespace(), target.getName())));
|
||||||
|
|||||||
@@ -323,8 +323,34 @@ class ResourceLinks {
|
|||||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
|
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String changesetDiff(NamespaceAndName namespaceAndName, String branch) {
|
}
|
||||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("changesetDiff").parameters(branch, "").href() + "{otherBranch}";
|
|
||||||
|
public IncomingLinks incoming() {
|
||||||
|
return new IncomingLinks(scmPathInfoStore.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
static class IncomingLinks {
|
||||||
|
private final LinkBuilder incomingLinkBuilder;
|
||||||
|
|
||||||
|
IncomingLinks(ScmPathInfo pathInfo) {
|
||||||
|
incomingLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, IncomingRootResource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String changesets(String namespace, String name) {
|
||||||
|
return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters("source","target").href());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String changesets(String namespace, String name, String source, String target) {
|
||||||
|
return incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingChangesets").parameters(source,target).href();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String diff(String namespace, String name) {
|
||||||
|
return toTemplateParams(incomingLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("incoming").parameters().method("incomingDiff").parameters("source", "target").href());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toTemplateParams(String href) {
|
||||||
|
return href.replace("source", "{source}").replace("target", "{target}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,4 +567,23 @@ class ResourceLinks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MergeLinks merge() {
|
||||||
|
return new MergeLinks(scmPathInfoStore.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
static class MergeLinks {
|
||||||
|
private final LinkBuilder mergeLinkBuilder;
|
||||||
|
|
||||||
|
MergeLinks(ScmPathInfo pathInfo) {
|
||||||
|
this.mergeLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, MergeResource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
String merge(String namespace, String name) {
|
||||||
|
return mergeLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("merge").parameters().method("merge").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
|
String dryRun(String namespace, String name) {
|
||||||
|
return mergeLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("merge").parameters().method("dryRun").parameters().href();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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.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.NotFoundException;
|
||||||
|
import sonia.scm.repository.Changeset;
|
||||||
|
import sonia.scm.repository.ChangesetPagingResult;
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||||
|
import sonia.scm.repository.api.LogCommandBuilder;
|
||||||
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||||
|
@Slf4j
|
||||||
|
public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||||
|
|
||||||
|
|
||||||
|
public static final String INCOMING_PATH = "space/repo/incoming/";
|
||||||
|
public static final String INCOMING_CHANGESETS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
|
||||||
|
public static final String INCOMING_DIFF_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + INCOMING_PATH;
|
||||||
|
|
||||||
|
private Dispatcher dispatcher;
|
||||||
|
|
||||||
|
private final URI baseUri = URI.create("/");
|
||||||
|
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RepositoryServiceFactory serviceFactory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RepositoryService repositoryService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LogCommandBuilder logCommandBuilder;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DiffCommandBuilder diffCommandBuilder;
|
||||||
|
|
||||||
|
|
||||||
|
private IncomingChangesetCollectionToDtoMapper incomingChangesetCollectionToDtoMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper;
|
||||||
|
|
||||||
|
private IncomingRootResource incomingRootResource;
|
||||||
|
|
||||||
|
|
||||||
|
private final Subject subject = mock(Subject.class);
|
||||||
|
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void prepareEnvironment() {
|
||||||
|
incomingChangesetCollectionToDtoMapper = new IncomingChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||||
|
incomingRootResource = new IncomingRootResource(serviceFactory, incomingChangesetCollectionToDtoMapper);
|
||||||
|
super.incomingRootResource = Providers.of(incomingRootResource);
|
||||||
|
dispatcher = DispatcherMock.createDispatcher(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"));
|
||||||
|
when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder);
|
||||||
|
when(repositoryService.getDiffCommand()).thenReturn(diffCommandBuilder);
|
||||||
|
dispatcher.getProviderFactory().registerProvider(CRLFInjectionExceptionMapper.class);
|
||||||
|
subjectThreadState.bind();
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void cleanupContext() {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetIncomingChangesets() throws Exception {
|
||||||
|
String id = "revision_123";
|
||||||
|
Instant creationDate = Instant.now();
|
||||||
|
String authorName = "name";
|
||||||
|
String authorEmail = "em@i.l";
|
||||||
|
String commit = "my branch commit";
|
||||||
|
ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class);
|
||||||
|
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
|
||||||
|
when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
|
||||||
|
when(changesetPagingResult.getTotal()).thenReturn(1);
|
||||||
|
when(logCommandBuilder.setPagingStart(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<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
|
||||||
|
when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
|
||||||
|
when(changesetPagingResult.getTotal()).thenReturn(1);
|
||||||
|
when(logCommandBuilder.setPagingStart(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.google.common.io.Resources;
|
||||||
|
import com.google.inject.util.Providers;
|
||||||
|
import org.jboss.resteasy.core.Dispatcher;
|
||||||
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.api.MergeCommandBuilder;
|
||||||
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
|
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||||
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
|
import sonia.scm.repository.spi.MergeCommand;
|
||||||
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class MergeResourceTest extends RepositoryTestBase {
|
||||||
|
|
||||||
|
public static final String MERGE_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/merge/";
|
||||||
|
|
||||||
|
private Dispatcher dispatcher;
|
||||||
|
@Mock
|
||||||
|
private RepositoryServiceFactory serviceFactory;
|
||||||
|
@Mock
|
||||||
|
private RepositoryService repositoryService;
|
||||||
|
@Mock
|
||||||
|
private MergeCommand mergeCommand;
|
||||||
|
@InjectMocks
|
||||||
|
private MergeCommandBuilder mergeCommandBuilder;
|
||||||
|
private MergeResultToDtoMapperImpl mapper = new MergeResultToDtoMapperImpl();
|
||||||
|
|
||||||
|
private MergeResource mergeResource;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init() {
|
||||||
|
mergeResource = new MergeResource(serviceFactory, mapper);
|
||||||
|
super.mergeResource = Providers.of(mergeResource);
|
||||||
|
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleIllegalInput() throws Exception {
|
||||||
|
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand_invalid.json");
|
||||||
|
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(MERGE_URL + "dry-run/")
|
||||||
|
.content(mergeCommandJson)
|
||||||
|
.contentType(VndMediaType.MERGE_COMMAND);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
|
System.out.println(response.getContentAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ExecutingMergeCommand {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initRepository() {
|
||||||
|
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||||
|
when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSuccessfulMerge() throws Exception {
|
||||||
|
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.success());
|
||||||
|
|
||||||
|
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||||
|
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(MERGE_URL)
|
||||||
|
.content(mergeCommandJson)
|
||||||
|
.contentType(VndMediaType.MERGE_COMMAND);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleFailedMerge() throws Exception {
|
||||||
|
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.failure(asList("file1", "file2")));
|
||||||
|
|
||||||
|
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||||
|
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(MERGE_URL)
|
||||||
|
.content(mergeCommandJson)
|
||||||
|
.contentType(VndMediaType.MERGE_COMMAND);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(409);
|
||||||
|
assertThat(response.getContentAsString()).contains("file1", "file2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSuccessfulDryRun() throws Exception {
|
||||||
|
when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(true));
|
||||||
|
|
||||||
|
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||||
|
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(MERGE_URL + "dry-run/")
|
||||||
|
.content(mergeCommandJson)
|
||||||
|
.contentType(VndMediaType.MERGE_COMMAND);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleFailedDryRun() throws Exception {
|
||||||
|
when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(false));
|
||||||
|
|
||||||
|
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||||
|
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(MERGE_URL + "dry-run/")
|
||||||
|
.content(mergeCommandJson)
|
||||||
|
.contentType(VndMediaType.MERGE_COMMAND);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ public abstract class RepositoryTestBase {
|
|||||||
protected Provider<ModificationsRootResource> modificationsRootResource;
|
protected Provider<ModificationsRootResource> modificationsRootResource;
|
||||||
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
|
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||||
protected Provider<RepositoryCollectionResource> repositoryCollectionResource;
|
protected Provider<RepositoryCollectionResource> repositoryCollectionResource;
|
||||||
|
protected Provider<IncomingRootResource> incomingRootResource;
|
||||||
|
protected Provider<MergeResource> mergeResource;
|
||||||
|
|
||||||
|
|
||||||
RepositoryRootResource getRepositoryRootResource() {
|
RepositoryRootResource getRepositoryRootResource() {
|
||||||
@@ -36,7 +38,9 @@ public abstract class RepositoryTestBase {
|
|||||||
permissionRootResource,
|
permissionRootResource,
|
||||||
diffRootResource,
|
diffRootResource,
|
||||||
modificationsRootResource,
|
modificationsRootResource,
|
||||||
fileHistoryRootResource)), repositoryCollectionResource);
|
fileHistoryRootResource,
|
||||||
|
incomingRootResource,
|
||||||
|
mergeResource)), repositoryCollectionResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class ResourceLinksMock {
|
|||||||
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
|
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
|
||||||
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));
|
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));
|
||||||
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo));
|
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo));
|
||||||
|
when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo));
|
||||||
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo));
|
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo));
|
||||||
when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo));
|
when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo));
|
||||||
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
|
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
|
||||||
@@ -37,6 +38,7 @@ public class ResourceLinksMock {
|
|||||||
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo));
|
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo));
|
||||||
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
|
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
|
||||||
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
|
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
|
||||||
|
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
|
||||||
|
|
||||||
return resourceLinks;
|
return resourceLinks;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sourceRevision": "source",
|
||||||
|
"targetRevision": "target"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sourceRevision": "",
|
||||||
|
"targetRevision": "target"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user