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:
19
scm-core/src/main/java/sonia/scm/DisplayManager.java
Normal file
19
scm-core/src/main/java/sonia/scm/DisplayManager.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package sonia.scm;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface DisplayManager<T extends ReducedModelObject> {
|
||||
|
||||
int DEFAULT_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* Returns a {@link Collection} of filtered objects
|
||||
*
|
||||
* @param filter the searched string
|
||||
* @return filtered object from the store
|
||||
*/
|
||||
Collection<T> autocomplete(String filter);
|
||||
|
||||
Optional<T> get(String id);
|
||||
}
|
||||
@@ -47,8 +47,6 @@ public interface Manager<T extends ModelObject>
|
||||
extends HandlerBase<T>, LastModifiedAware
|
||||
{
|
||||
|
||||
int DEFAULT_LIMIT = 5;
|
||||
|
||||
|
||||
/**
|
||||
* Reloads a object from store and overwrites all changes.
|
||||
|
||||
@@ -39,8 +39,10 @@ package sonia.scm;
|
||||
* @author Sebastian Sdorra
|
||||
*
|
||||
* @param <T> type of objects to transform
|
||||
* @param <R> result type of the transformation
|
||||
*/
|
||||
public interface TransformFilter<T>
|
||||
@FunctionalInterface
|
||||
public interface TransformFilter<T, R>
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -52,5 +54,5 @@ public interface TransformFilter<T>
|
||||
*
|
||||
* @return tranformed object
|
||||
*/
|
||||
public T accept(T item);
|
||||
R accept(T item);
|
||||
}
|
||||
|
||||
26
scm-core/src/main/java/sonia/scm/group/DisplayGroup.java
Normal file
26
scm-core/src/main/java/sonia/scm/group/DisplayGroup.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package sonia.scm.group;
|
||||
|
||||
import sonia.scm.ReducedModelObject;
|
||||
|
||||
public class DisplayGroup implements ReducedModelObject {
|
||||
|
||||
private final String id;
|
||||
private final String displayName;
|
||||
|
||||
public static DisplayGroup from(Group group) {
|
||||
return new DisplayGroup(group.getId(), group.getDescription());
|
||||
}
|
||||
|
||||
private DisplayGroup(String id, String displayName) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package sonia.scm.group;
|
||||
|
||||
import sonia.scm.DisplayManager;
|
||||
|
||||
public interface GroupDisplayManager extends DisplayManager<DisplayGroup> {
|
||||
}
|
||||
@@ -61,14 +61,4 @@ public interface GroupManager
|
||||
* @return all groups assigned to the given member
|
||||
*/
|
||||
public Collection<Group> getGroupsForMember(String member);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a {@link java.util.Collection} of filtered objects
|
||||
*
|
||||
* @param filter the searched string
|
||||
* @return filtered object from the store
|
||||
*/
|
||||
Collection<Group> autocomplete(String filter);
|
||||
|
||||
}
|
||||
|
||||
@@ -109,11 +109,6 @@ public class GroupManagerDecorator
|
||||
return decorated.getGroupsForMember(member);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Group> autocomplete(String filter) {
|
||||
return decorated.autocomplete(filter);
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
|
||||
@@ -66,7 +66,7 @@ public final class Branch implements Serializable
|
||||
* This constructor should only be called from JAXB.
|
||||
*
|
||||
*/
|
||||
public Branch() {}
|
||||
Branch() {}
|
||||
|
||||
/**
|
||||
* Constructs a new branch.
|
||||
@@ -75,10 +75,19 @@ public final class Branch implements Serializable
|
||||
* @param name name of the branch
|
||||
* @param revision latest revision of the branch
|
||||
*/
|
||||
public Branch(String name, String revision)
|
||||
Branch(String name, String revision, boolean defaultBranch)
|
||||
{
|
||||
this.name = name;
|
||||
this.revision = revision;
|
||||
this.defaultBranch = defaultBranch;
|
||||
}
|
||||
|
||||
public static Branch normalBranch(String name, String revision) {
|
||||
return new Branch(name, revision, false);
|
||||
}
|
||||
|
||||
public static Branch defaultBranch(String name, String revision) {
|
||||
return new Branch(name, revision, true);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -107,7 +116,8 @@ public final class Branch implements Serializable
|
||||
final Branch other = (Branch) obj;
|
||||
|
||||
return Objects.equal(name, other.name)
|
||||
&& Objects.equal(revision, other.revision);
|
||||
&& Objects.equal(revision, other.revision)
|
||||
&& Objects.equal(defaultBranch, other.defaultBranch);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +172,10 @@ public final class Branch implements Serializable
|
||||
return revision;
|
||||
}
|
||||
|
||||
public boolean isDefaultBranch() {
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** name of the branch */
|
||||
@@ -169,4 +183,6 @@ public final class Branch implements Serializable
|
||||
|
||||
/** Field description */
|
||||
private String revision;
|
||||
|
||||
private boolean defaultBranch;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.spi.BranchCommand;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
public final class BranchCommandBuilder {
|
||||
|
||||
public BranchCommandBuilder(BranchCommand command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the source branch, which the new branch should be based on.
|
||||
*
|
||||
* @param parentBranch The base branch for the new branch.
|
||||
* @return This builder.
|
||||
*/
|
||||
public BranchCommandBuilder from(String parentBranch) {
|
||||
request.setParentBranch(parentBranch);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command and create a new branch with the given name.
|
||||
* @param name The name of the new branch.
|
||||
* @return The created branch.
|
||||
* @throws IOException
|
||||
*/
|
||||
public Branch branch(String name) {
|
||||
request.setNewBranch(name);
|
||||
return command.branch(request);
|
||||
}
|
||||
|
||||
private BranchCommand command;
|
||||
private BranchRequest request = new BranchRequest();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
public class BranchRequest {
|
||||
private String parentBranch;
|
||||
private String newBranch;
|
||||
|
||||
public String getParentBranch() {
|
||||
return parentBranch;
|
||||
}
|
||||
|
||||
public void setParentBranch(String parentBranch) {
|
||||
this.parentBranch = parentBranch;
|
||||
}
|
||||
|
||||
public String getNewBranch() {
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
public void setNewBranch(String newBranch) {
|
||||
this.newBranch = newBranch;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,11 @@ public enum Command
|
||||
*/
|
||||
BRANCHES,
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
BRANCH,
|
||||
|
||||
/**
|
||||
* @since 1.31
|
||||
*/
|
||||
|
||||
@@ -46,6 +46,7 @@ import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.spi.HookChangesetProvider;
|
||||
import sonia.scm.repository.spi.HookChangesetRequest;
|
||||
import sonia.scm.repository.spi.HookChangesetResponse;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -115,8 +116,8 @@ public final class HookChangesetBuilder
|
||||
*/
|
||||
public Iterable<Changeset> getChangesets()
|
||||
{
|
||||
Iterable<Changeset> changesets =
|
||||
provider.handleRequest(request).getChangesets();
|
||||
HookChangesetResponse hookChangesetResponse = provider.handleRequest(request);
|
||||
Iterable<Changeset> changesets = hookChangesetResponse.getChangesets();
|
||||
|
||||
if (!disablePreProcessors)
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||
|
||||
import java.io.Closeable;
|
||||
@@ -82,10 +83,9 @@ import java.util.stream.Stream;
|
||||
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
|
||||
* @since 1.17
|
||||
*/
|
||||
@Slf4j
|
||||
public final class RepositoryService implements Closeable {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepositoryService.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RepositoryService.class);
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
private final PreProcessorUtil preProcessorUtil;
|
||||
@@ -131,7 +131,7 @@ public final class RepositoryService implements Closeable {
|
||||
try {
|
||||
provider.close();
|
||||
} catch (IOException ex) {
|
||||
log.error("Could not close repository service provider", ex);
|
||||
LOG.error("Could not close repository service provider", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BlameCommandBuilder getBlameCommand() {
|
||||
logger.debug("create blame command for repository {}",
|
||||
LOG.debug("create blame command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
|
||||
@@ -158,13 +158,28 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BranchesCommandBuilder getBranchesCommand() {
|
||||
logger.debug("create branches command for repository {}",
|
||||
LOG.debug("create branches command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new BranchesCommandBuilder(cacheManager,
|
||||
provider.getBranchesCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch command creates new branches.
|
||||
*
|
||||
* @return instance of {@link BranchCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BranchCommandBuilder getBranchCommand() {
|
||||
RepositoryPermissions.push(getRepository()).check();
|
||||
LOG.debug("create branch command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new BranchCommandBuilder(provider.getBranchCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* The browse command allows browsing of a repository.
|
||||
*
|
||||
@@ -173,7 +188,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BrowseCommandBuilder getBrowseCommand() {
|
||||
logger.debug("create browse command for repository {}",
|
||||
LOG.debug("create browse command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
|
||||
@@ -189,7 +204,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.43
|
||||
*/
|
||||
public BundleCommandBuilder getBundleCommand() {
|
||||
logger.debug("create bundle command for repository {}",
|
||||
LOG.debug("create bundle command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new BundleCommandBuilder(provider.getBundleCommand(), repository);
|
||||
@@ -203,7 +218,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public CatCommandBuilder getCatCommand() {
|
||||
logger.debug("create cat command for repository {}",
|
||||
LOG.debug("create cat command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new CatCommandBuilder(provider.getCatCommand());
|
||||
@@ -218,7 +233,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public DiffCommandBuilder getDiffCommand() {
|
||||
logger.debug("create diff command for repository {}",
|
||||
LOG.debug("create diff command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
|
||||
@@ -234,7 +249,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.31
|
||||
*/
|
||||
public IncomingCommandBuilder getIncomingCommand() {
|
||||
logger.debug("create incoming command for repository {}",
|
||||
LOG.debug("create incoming command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new IncomingCommandBuilder(cacheManager,
|
||||
@@ -249,7 +264,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public LogCommandBuilder getLogCommand() {
|
||||
logger.debug("create log command for repository {}",
|
||||
LOG.debug("create log command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
|
||||
@@ -264,7 +279,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public ModificationsCommandBuilder getModificationsCommand() {
|
||||
logger.debug("create modifications command for repository {}", repository.getNamespaceAndName());
|
||||
LOG.debug("create modifications command for repository {}", repository.getNamespaceAndName());
|
||||
return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
|
||||
}
|
||||
|
||||
@@ -277,7 +292,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.31
|
||||
*/
|
||||
public OutgoingCommandBuilder getOutgoingCommand() {
|
||||
logger.debug("create outgoing command for repository {}",
|
||||
LOG.debug("create outgoing command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new OutgoingCommandBuilder(cacheManager,
|
||||
@@ -293,7 +308,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.31
|
||||
*/
|
||||
public PullCommandBuilder getPullCommand() {
|
||||
logger.debug("create pull command for repository {}",
|
||||
LOG.debug("create pull command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new PullCommandBuilder(provider.getPullCommand(), repository);
|
||||
@@ -308,7 +323,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.31
|
||||
*/
|
||||
public PushCommandBuilder getPushCommand() {
|
||||
logger.debug("create push command for repository {}",
|
||||
LOG.debug("create push command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new PushCommandBuilder(provider.getPushCommand());
|
||||
@@ -331,7 +346,7 @@ public final class RepositoryService implements Closeable {
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public TagsCommandBuilder getTagsCommand() {
|
||||
logger.debug("create tags command for repository {}",
|
||||
LOG.debug("create tags command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
|
||||
@@ -347,7 +362,7 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 1.43
|
||||
*/
|
||||
public UnbundleCommandBuilder getUnbundleCommand() {
|
||||
logger.debug("create unbundle command for repository {}",
|
||||
LOG.debug("create unbundle command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
|
||||
@@ -364,7 +379,8 @@ public final class RepositoryService implements Closeable {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public MergeCommandBuilder getMergeCommand() {
|
||||
logger.debug("create merge command for repository {}",
|
||||
RepositoryPermissions.push(getRepository()).check();
|
||||
LOG.debug("create merge command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new MergeCommandBuilder(provider.getMergeCommand());
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface BranchCommand {
|
||||
Branch branch(BranchRequest name);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
public class IntegrateChangesFromWorkdirException extends ExceptionWithContext {
|
||||
|
||||
private static final String CODE = "CHRM7IQzo1";
|
||||
|
||||
public IntegrateChangesFromWorkdirException(Repository repository, String message) {
|
||||
super(ContextEntry.ContextBuilder.entity(repository).build(), message);
|
||||
}
|
||||
|
||||
public IntegrateChangesFromWorkdirException(Repository repository, String message, Exception cause) {
|
||||
super(ContextEntry.ContextBuilder.entity(repository).build(), message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,17 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
throw new CommandNotSupportedException(Command.BRANCHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public BranchCommand getBranchCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.BRANCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class CloseableWrapper<T extends AutoCloseable> implements AutoCloseable {
|
||||
|
||||
private final T wrapped;
|
||||
private final Consumer<T> cleanup;
|
||||
|
||||
public CloseableWrapper(T wrapped, Consumer<T> cleanup) {
|
||||
this.wrapped = wrapped;
|
||||
this.cleanup = cleanup;
|
||||
}
|
||||
|
||||
public T get() { return wrapped; }
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cleanup.accept(wrapped);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public abstract class SimpleWorkdirFactory<R, C> implements WorkdirFactory<R, C> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class);
|
||||
|
||||
private final File poolDirectory;
|
||||
|
||||
public SimpleWorkdirFactory() {
|
||||
this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work"));
|
||||
}
|
||||
|
||||
public SimpleWorkdirFactory(File poolDirectory) {
|
||||
this.poolDirectory = poolDirectory;
|
||||
if (!poolDirectory.exists() && !poolDirectory.mkdirs()) {
|
||||
throw new IllegalStateException("could not create pool directory " + poolDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkingCopy<R> createWorkingCopy(C context) {
|
||||
try {
|
||||
File directory = createNewWorkdir();
|
||||
ParentAndClone<R> parentAndClone = cloneRepository(context, directory);
|
||||
return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(getScmRepository(context), "could not clone repository in temporary directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Repository getScmRepository(C context);
|
||||
|
||||
@SuppressWarnings("squid:S00112")
|
||||
// We do allow implementations to throw arbitrary exceptions here, so that we can handle them in close
|
||||
protected abstract void closeRepository(R repository) throws Exception;
|
||||
|
||||
protected abstract ParentAndClone<R> cloneRepository(C context, File target) throws IOException;
|
||||
|
||||
private File createNewWorkdir() throws IOException {
|
||||
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
|
||||
}
|
||||
|
||||
private void close(R repository) {
|
||||
try {
|
||||
closeRepository(repository);
|
||||
} catch (Exception e) {
|
||||
logger.warn("could not close temporary repository clone", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class ParentAndClone<R> {
|
||||
private final R parent;
|
||||
private final R clone;
|
||||
|
||||
public ParentAndClone(R parent, R clone) {
|
||||
this.parent = parent;
|
||||
this.clone = clone;
|
||||
}
|
||||
|
||||
public R getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public R getClone() {
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
public interface WorkdirFactory<R, C> {
|
||||
WorkingCopy<R> createWorkingCopy(C context);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class WorkingCopy<R> implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WorkingCopy.class);
|
||||
|
||||
private final File directory;
|
||||
private final R workingRepository;
|
||||
private final R centralRepository;
|
||||
private final Consumer<R> cleanup;
|
||||
|
||||
public WorkingCopy(R workingRepository, R centralRepository, Consumer<R> cleanup, File directory) {
|
||||
this.directory = directory;
|
||||
this.workingRepository = workingRepository;
|
||||
this.centralRepository = centralRepository;
|
||||
this.cleanup = cleanup;
|
||||
}
|
||||
|
||||
public R getWorkingRepository() {
|
||||
return workingRepository;
|
||||
}
|
||||
|
||||
public R getCentralRepository() {
|
||||
return centralRepository;
|
||||
}
|
||||
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
cleanup.accept(workingRepository);
|
||||
cleanup.accept(centralRepository);
|
||||
IOUtil.delete(directory);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete temporary workdir '{}'", directory, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,17 +159,17 @@ public final class SearchUtil
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static <T> Collection<T> search(SearchRequest searchRequest,
|
||||
Collection<T> collection, TransformFilter<T> filter)
|
||||
public static <T, R> Collection<R> search(SearchRequest searchRequest,
|
||||
Collection<T> collection, TransformFilter<T, R> filter)
|
||||
{
|
||||
List<T> items = new ArrayList<T>();
|
||||
List<R> items = new ArrayList<>();
|
||||
int index = 0;
|
||||
int counter = 0;
|
||||
Iterator<T> it = collection.iterator();
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
T item = filter.accept(it.next());
|
||||
R item = filter.accept(it.next());
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
|
||||
32
scm-core/src/main/java/sonia/scm/user/DisplayUser.java
Normal file
32
scm-core/src/main/java/sonia/scm/user/DisplayUser.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package sonia.scm.user;
|
||||
|
||||
import sonia.scm.ReducedModelObject;
|
||||
|
||||
public class DisplayUser implements ReducedModelObject {
|
||||
|
||||
private final String id;
|
||||
private final String displayName;
|
||||
private final String mail;
|
||||
|
||||
public static DisplayUser from(User user) {
|
||||
return new DisplayUser(user.getId(), user.getDisplayName(), user.getMail());
|
||||
}
|
||||
|
||||
private DisplayUser(String id, String displayName, String mail) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
this.mail = mail;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getMail() {
|
||||
return mail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package sonia.scm.user;
|
||||
|
||||
import sonia.scm.DisplayManager;
|
||||
|
||||
public interface UserDisplayManager extends DisplayManager<DisplayUser> {
|
||||
}
|
||||
@@ -75,14 +75,6 @@ public interface UserManager
|
||||
return getDefaultType().equals(user.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link java.util.Collection} of filtered objects
|
||||
*
|
||||
* @param filter the searched string
|
||||
* @return filtered object from the store
|
||||
*/
|
||||
Collection<User> autocomplete(String filter);
|
||||
|
||||
/**
|
||||
* Changes the password of the logged in user.
|
||||
* @param oldPassword The current encrypted password of the user.
|
||||
|
||||
@@ -121,11 +121,6 @@ public class UserManagerDecorator extends ManagerDecorator<User>
|
||||
return decorated.getDefaultType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<User> autocomplete(String filter) {
|
||||
return decorated.autocomplete(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
|
||||
decorated.changePasswordForLoggedInUser(oldPassword, newPassword);
|
||||
|
||||
@@ -27,6 +27,7 @@ public class VndMediaType {
|
||||
public static final String TAG = PREFIX + "tag" + SUFFIX;
|
||||
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
|
||||
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
|
||||
public static final String BRANCH_REQUEST = PREFIX + "branchRequest" + SUFFIX;
|
||||
public static final String DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX;
|
||||
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
|
||||
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class SimpleWorkdirFactoryTest {
|
||||
|
||||
private static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
|
||||
|
||||
private final Closeable parent = mock(Closeable.class);
|
||||
private final Closeable clone = mock(Closeable.class);
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
private SimpleWorkdirFactory<Closeable, Context> simpleWorkdirFactory;
|
||||
|
||||
@Before
|
||||
public void initFactory() throws IOException {
|
||||
simpleWorkdirFactory = new SimpleWorkdirFactory<Closeable, Context>(temporaryFolder.newFolder()) {
|
||||
@Override
|
||||
protected Repository getScmRepository(Context context) {
|
||||
return REPOSITORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeRepository(Closeable repository) throws IOException {
|
||||
repository.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ParentAndClone<Closeable> cloneRepository(Context context, File target) {
|
||||
return new ParentAndClone<>(parent, clone);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateParentAndClone() {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {
|
||||
assertThat(workingCopy.getCentralRepository()).isSameAs(parent);
|
||||
assertThat(workingCopy.getWorkingRepository()).isSameAs(clone);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCloseParent() throws IOException {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {}
|
||||
|
||||
verify(parent).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCloseClone() throws IOException {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {}
|
||||
|
||||
verify(clone).close();
|
||||
}
|
||||
|
||||
private static class Context {}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class CloseableWrapper<C> implements AutoCloseable {
|
||||
|
||||
private final C wrapped;
|
||||
private final Consumer<C> cleanup;
|
||||
|
||||
public CloseableWrapper(C wrapped, Consumer<C> cleanup) {
|
||||
this.wrapped = wrapped;
|
||||
this.cleanup = cleanup;
|
||||
}
|
||||
|
||||
public C get() { return wrapped; }
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
cleanup.accept(wrapped);
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import sonia.scm.repository.spi.GitContext;
|
||||
import sonia.scm.repository.spi.WorkingCopy;
|
||||
import sonia.scm.repository.util.WorkdirFactory;
|
||||
|
||||
public interface GitWorkdirFactory {
|
||||
WorkingCopy createWorkingCopy(GitContext gitContext);
|
||||
public interface GitWorkdirFactory extends WorkdirFactory<Repository, GitContext> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.transport.PushResult;
|
||||
import org.eclipse.jgit.transport.RemoteRefUpdate;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class GitBranchCommand extends AbstractGitCommand implements BranchCommand {
|
||||
|
||||
private final GitWorkdirFactory workdirFactory;
|
||||
|
||||
GitBranchCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) {
|
||||
super(context, repository);
|
||||
this.workdirFactory = workdirFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Branch branch(BranchRequest request) {
|
||||
try (WorkingCopy<org.eclipse.jgit.lib.Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
Git clone = new Git(workingCopy.getWorkingRepository());
|
||||
if (request.getParentBranch() != null) {
|
||||
clone.checkout().setName(request.getParentBranch());
|
||||
}
|
||||
Ref ref = clone.branchCreate().setName(request.getNewBranch()).call();
|
||||
Iterable<PushResult> call = clone.push().add(request.getNewBranch()).call();
|
||||
StreamSupport.stream(call.spliterator(), false)
|
||||
.flatMap(pushResult -> pushResult.getRemoteUpdates().stream())
|
||||
.filter(remoteRefUpdate -> remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK)
|
||||
.findFirst()
|
||||
.ifPresent(r -> this.handlePushError(r, request, context.getRepository()));
|
||||
return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId()));
|
||||
} catch (GitAPIException ex) {
|
||||
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePushError(RemoteRefUpdate remoteRefUpdate, BranchRequest request, Repository repository) {
|
||||
if (remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) {
|
||||
// TODO handle failed remote update
|
||||
throw new IntegrateChangesFromWorkdirException(repository,
|
||||
String.format("Could not push new branch '%s' into central repository", request.getNewBranch()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,14 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
@@ -46,6 +49,8 @@ import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -53,17 +58,10 @@ import java.util.List;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class GitBranchesCommand extends AbstractGitCommand
|
||||
implements BranchesCommand
|
||||
{
|
||||
public class GitBranchesCommand extends AbstractGitCommand implements BranchesCommand {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class);
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param context
|
||||
* @param repository
|
||||
*/
|
||||
public GitBranchesCommand(GitContext context, Repository repository)
|
||||
{
|
||||
super(context, repository);
|
||||
@@ -73,38 +71,54 @@ public class GitBranchesCommand extends AbstractGitCommand
|
||||
|
||||
@Override
|
||||
public List<Branch> getBranches() throws IOException {
|
||||
List<Branch> branches = null;
|
||||
Git git = createGit();
|
||||
|
||||
Git git = new Git(open());
|
||||
String defaultBranchName = determineDefaultBranchName(git);
|
||||
|
||||
try
|
||||
{
|
||||
List<Ref> refs = git.branchList().call();
|
||||
|
||||
branches = Lists.transform(refs, new Function<Ref, Branch>()
|
||||
{
|
||||
|
||||
@Override
|
||||
public Branch apply(Ref ref)
|
||||
{
|
||||
Branch branch = null;
|
||||
String branchName = GitUtil.getBranch(ref);
|
||||
|
||||
if (branchName != null)
|
||||
{
|
||||
branch = new Branch(branchName, GitUtil.getId(ref.getObjectId()));
|
||||
}
|
||||
|
||||
return branch;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
catch (GitAPIException ex)
|
||||
{
|
||||
try {
|
||||
return git
|
||||
.branchList()
|
||||
.call()
|
||||
.stream()
|
||||
.map(ref -> createBranchObject(defaultBranchName, ref))
|
||||
.collect(Collectors.toList());
|
||||
} catch (GitAPIException ex) {
|
||||
throw new InternalRepositoryException(repository, "could not read branches", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return branches;
|
||||
@VisibleForTesting
|
||||
Git createGit() throws IOException {
|
||||
return new Git(open());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Branch createBranchObject(String defaultBranchName, Ref ref) {
|
||||
String branchName = GitUtil.getBranch(ref);
|
||||
|
||||
if (branchName == null) {
|
||||
LOG.warn("could not determine branch name for branch name {} at revision {}", ref.getName(), ref.getObjectId());
|
||||
return null;
|
||||
} else {
|
||||
if (branchName.equals(defaultBranchName)) {
|
||||
return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId()));
|
||||
} else {
|
||||
return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String determineDefaultBranchName(Git git) {
|
||||
String defaultBranchName = context.getConfig().getDefaultBranch();
|
||||
if (Strings.isNullOrEmpty(defaultBranchName)) {
|
||||
return getRepositoryHeadRef(git).map(GitUtil::getBranch).orElse(null);
|
||||
} else {
|
||||
return defaultBranchName;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Ref> getRepositoryHeadRef(Git git) {
|
||||
return GitUtil.getRepositoryHeadRef(git.getRepository());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -46,10 +47,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
|
||||
@Override
|
||||
public MergeCommandResult merge(MergeCommandRequest request) {
|
||||
RepositoryPermissions.push(context.getRepository().getId()).check();
|
||||
|
||||
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
Repository repository = workingCopy.get();
|
||||
try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
Repository repository = workingCopy.getWorkingRepository();
|
||||
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
||||
return new MergeWorker(repository, request).merge();
|
||||
} catch (IOException e) {
|
||||
@@ -186,7 +185,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
try {
|
||||
clone.push().call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + target + " to origin", e);
|
||||
throw new IntegrateChangesFromWorkdirException(repository,
|
||||
"could not push merged branch " + target + " into central repository", e);
|
||||
}
|
||||
logger.debug("pushed merged branch {}", target);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,18 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
return new GitBranchesCommand(context, repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public BranchCommand getBranchCommand()
|
||||
{
|
||||
return new GitBranchCommand(context, repository, handler.getWorkdirFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -4,64 +4,49 @@ import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import org.eclipse.jgit.util.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.util.SimpleWorkdirFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class SimpleGitWorkdirFactory implements GitWorkdirFactory {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SimpleGitWorkdirFactory.class);
|
||||
|
||||
private final File poolDirectory;
|
||||
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
|
||||
|
||||
public SimpleGitWorkdirFactory() {
|
||||
this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
|
||||
}
|
||||
|
||||
public SimpleGitWorkdirFactory(File poolDirectory) {
|
||||
this.poolDirectory = poolDirectory;
|
||||
poolDirectory.mkdirs();
|
||||
SimpleGitWorkdirFactory(File poolDirectory) {
|
||||
super(poolDirectory);
|
||||
}
|
||||
|
||||
public WorkingCopy createWorkingCopy(GitContext gitContext) {
|
||||
@Override
|
||||
public ParentAndClone<Repository> cloneRepository(GitContext context, File target) {
|
||||
try {
|
||||
Repository clone = cloneRepository(gitContext.getDirectory(), createNewWorkdir());
|
||||
return new WorkingCopy(clone, this::close);
|
||||
return new ParentAndClone<>(null, Git.cloneRepository()
|
||||
.setURI(createScmTransportProtocolUri(context.getDirectory()))
|
||||
.setDirectory(target)
|
||||
.call()
|
||||
.getRepository());
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(gitContext.getRepository(), "could not clone working copy of repository", e);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(gitContext.getRepository(), "could not create temporary directory for clone of repository", e);
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File createNewWorkdir() throws IOException {
|
||||
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
|
||||
}
|
||||
|
||||
protected Repository cloneRepository(File bareRepository, File target) throws GitAPIException {
|
||||
return Git.cloneRepository()
|
||||
.setURI(createScmTransportProtocolUri(bareRepository))
|
||||
.setDirectory(target)
|
||||
.call()
|
||||
.getRepository();
|
||||
}
|
||||
|
||||
private String createScmTransportProtocolUri(File bareRepository) {
|
||||
return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath();
|
||||
}
|
||||
|
||||
private void close(Repository repository) {
|
||||
repository.close();
|
||||
try {
|
||||
FileUtils.delete(repository.getWorkTree(), FileUtils.RECURSIVE);
|
||||
} catch (IOException e) {
|
||||
logger.warn("could not delete temporary git workdir '{}'", repository.getWorkTree(), e);
|
||||
@Override
|
||||
protected void closeRepository(Repository repository) {
|
||||
// we have to check for null here, because we do not create a repository for
|
||||
// the parent in cloneRepository
|
||||
if (repository != null) {
|
||||
repository.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected sonia.scm.repository.Repository getScmRepository(GitContext context) {
|
||||
return context.getRepository();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import sonia.scm.repository.CloseableWrapper;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class WorkingCopy extends CloseableWrapper<Repository> {
|
||||
WorkingCopy(Repository wrapped, Consumer<Repository> cleanup) {
|
||||
super(wrapped, cleanup);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.util.CloseableWrapper;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -11,19 +12,20 @@ public class CloseableWrapperTest {
|
||||
|
||||
@Test
|
||||
public void shouldExecuteGivenMethodAtClose() {
|
||||
Consumer<String> wrapped = new Consumer<String>() {
|
||||
Consumer<AutoCloseable> wrapped = new Consumer<AutoCloseable>() {
|
||||
// no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy
|
||||
@Override
|
||||
public void accept(String s) {
|
||||
public void accept(AutoCloseable s) {
|
||||
}
|
||||
};
|
||||
|
||||
Consumer<String> closer = spy(wrapped);
|
||||
Consumer<AutoCloseable> closer = spy(wrapped);
|
||||
|
||||
try (CloseableWrapper<String> wrapper = new CloseableWrapper<>("test", closer)) {
|
||||
AutoCloseable autoCloseable = () -> {};
|
||||
try (CloseableWrapper<AutoCloseable> wrapper = new CloseableWrapper<>(autoCloseable, closer)) {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
verify(closer).accept("test");
|
||||
verify(closer).accept(autoCloseable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class GitBranchCommand implements BranchCommand
|
||||
try
|
||||
{
|
||||
Ref ref = git.branchCreate().setName(name).call();
|
||||
return new Branch(name, GitUtil.getId(ref.getObjectId()));
|
||||
return Branch.normalBranch(name, GitUtil.getId(ref.getObjectId()));
|
||||
}
|
||||
catch (GitAPIException ex)
|
||||
{
|
||||
|
||||
@@ -34,11 +34,19 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import org.eclipse.jgit.transport.Transport;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||
import sonia.scm.repository.GitRepositoryConfig;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||
|
||||
import static com.google.inject.util.Providers.of;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -105,4 +113,5 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
|
||||
|
||||
/** Field description */
|
||||
private GitContext context;
|
||||
private ScmTransportProtocol scmTransportProtocol;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import org.eclipse.jgit.transport.Transport;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
|
||||
import static com.google.inject.util.Providers.of;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class BindTransportProtocolRule extends ExternalResource {
|
||||
|
||||
private ScmTransportProtocol scmTransportProtocol;
|
||||
|
||||
@Override
|
||||
protected void before() throws Throwable {
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
Transport.unregister(scmTransportProtocol);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class GitBranchCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Rule
|
||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
||||
|
||||
@Test
|
||||
public void shouldCreateBranch() throws IOException {
|
||||
GitContext context = createContext();
|
||||
|
||||
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
|
||||
|
||||
BranchRequest branchRequest = new BranchRequest();
|
||||
branchRequest.setNewBranch("new_branch");
|
||||
|
||||
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest);
|
||||
|
||||
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
|
||||
}
|
||||
|
||||
private List<Branch> readBranches(GitContext context) throws IOException {
|
||||
return new GitBranchesCommand(context, repository).getBranches();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.ListBranchCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.GitRepositoryConfig;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GitBranchesCommandTest {
|
||||
|
||||
@Mock
|
||||
GitContext context;
|
||||
@Mock
|
||||
Git git;
|
||||
@Mock
|
||||
ListBranchCommand listBranchCommand;
|
||||
@Mock
|
||||
GitRepositoryConfig gitRepositoryConfig;
|
||||
|
||||
GitBranchesCommand branchesCommand;
|
||||
private Ref master;
|
||||
|
||||
@BeforeEach
|
||||
void initContext() {
|
||||
when(context.getConfig()).thenReturn(gitRepositoryConfig);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initCommand() {
|
||||
master = createRef("master", "0000");
|
||||
branchesCommand = new GitBranchesCommand(context, new Repository("1", "git", "space", "X")) {
|
||||
@Override
|
||||
Git createGit() {
|
||||
return git;
|
||||
}
|
||||
|
||||
@Override
|
||||
Optional<Ref> getRepositoryHeadRef(Git git) {
|
||||
return of(master);
|
||||
}
|
||||
};
|
||||
when(git.branchList()).thenReturn(listBranchCommand);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateEmptyListWithoutBranches() throws IOException, GitAPIException {
|
||||
when(listBranchCommand.call()).thenReturn(emptyList());
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapNormalBranch() throws IOException, GitAPIException {
|
||||
Ref branch = createRef("branch", "1337");
|
||||
when(listBranchCommand.call()).thenReturn(asList(branch));
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).containsExactly(Branch.normalBranch("branch", "1337"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkMasterBranchWithMasterFromConfig() throws IOException, GitAPIException {
|
||||
Ref branch = createRef("branch", "1337");
|
||||
when(listBranchCommand.call()).thenReturn(asList(branch));
|
||||
when(gitRepositoryConfig.getDefaultBranch()).thenReturn("branch");
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).containsExactlyInAnyOrder(Branch.defaultBranch("branch", "1337"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkMasterBranchWithMasterFromHead() throws IOException, GitAPIException {
|
||||
Ref branch = createRef("branch", "1337");
|
||||
when(listBranchCommand.call()).thenReturn(asList(branch, master));
|
||||
|
||||
List<Branch> branches = branchesCommand.getBranches();
|
||||
|
||||
assertThat(branches).containsExactlyInAnyOrder(
|
||||
Branch.normalBranch("branch", "1337"),
|
||||
Branch.defaultBranch("master", "0000")
|
||||
);
|
||||
}
|
||||
|
||||
private Ref createRef(String branchName, String revision) {
|
||||
Ref ref = mock(Ref.class);
|
||||
lenient().when(ref.getName()).thenReturn("refs/heads/" + branchName);
|
||||
ObjectId objectId = mock(ObjectId.class);
|
||||
lenient().when(objectId.name()).thenReturn(revision);
|
||||
lenient().when(ref.getObjectId()).thenReturn(objectId);
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
@@ -41,27 +41,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Rule
|
||||
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);
|
||||
}
|
||||
@Rule
|
||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
||||
|
||||
@Test
|
||||
public void shouldDetectMergeableBranches() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import org.eclipse.jgit.transport.Transport;
|
||||
@@ -12,6 +11,7 @@ import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -44,39 +44,29 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||
File masterRepo = createRepositoryDirectory();
|
||||
|
||||
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
|
||||
assertThat(workingCopy.get().getDirectory())
|
||||
assertThat(workingCopy.getDirectory())
|
||||
.exists()
|
||||
.isNotEqualTo(masterRepo)
|
||||
.isDirectory();
|
||||
assertThat(new File(workingCopy.get().getWorkTree(), "a.txt"))
|
||||
assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt"))
|
||||
.exists()
|
||||
.isFile()
|
||||
.hasContent("a\nline for blame");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldBeClosed() throws IOException {
|
||||
PoolWithSpy factory = new PoolWithSpy(temporaryFolder.newFolder());
|
||||
|
||||
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
assertThat(workingCopy).isNotNull();
|
||||
}
|
||||
verify(factory.createdClone).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldNotBeReused() throws IOException {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||
|
||||
File firstDirectory;
|
||||
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
firstDirectory = workingCopy.get().getDirectory();
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
firstDirectory = workingCopy.getDirectory();
|
||||
}
|
||||
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
File secondDirectory = workingCopy.get().getDirectory();
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
File secondDirectory = workingCopy.getDirectory();
|
||||
assertThat(secondDirectory).isNotEqualTo(firstDirectory);
|
||||
}
|
||||
}
|
||||
@@ -86,23 +76,9 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||
|
||||
File directory;
|
||||
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
directory = workingCopy.get().getWorkTree();
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
directory = workingCopy.getWorkingRepository().getWorkTree();
|
||||
}
|
||||
assertThat(directory).doesNotExist();
|
||||
}
|
||||
|
||||
private static class PoolWithSpy extends SimpleGitWorkdirFactory {
|
||||
PoolWithSpy(File poolDirectory) {
|
||||
super(poolDirectory);
|
||||
}
|
||||
|
||||
Repository createdClone;
|
||||
|
||||
@Override
|
||||
protected Repository cloneRepository(File bareRepository, File destination) throws GitAPIException {
|
||||
createdClone = spy(super.cloneRepository(bareRepository, destination));
|
||||
return createdClone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-inline
|
||||
@@ -28,7 +28,6 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -35,15 +35,9 @@ package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.apache.shiro.codec.Base64;
|
||||
|
||||
import com.google.inject.ProvisionException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.HgUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -59,6 +53,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
public final class HgEnvironment
|
||||
{
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HgEnvironment.class);
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_PYTHON_PATH = "PYTHONPATH";
|
||||
|
||||
@@ -68,14 +64,7 @@ public final class HgEnvironment
|
||||
/** Field description */
|
||||
private static final String ENV_URL = "SCM_URL";
|
||||
|
||||
/** Field description */
|
||||
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
|
||||
|
||||
/**
|
||||
* the logger for HgEnvironment
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(HgEnvironment.class);
|
||||
private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -87,6 +76,20 @@ public final class HgEnvironment
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param environment
|
||||
* @param handler
|
||||
* @param hookManager
|
||||
*/
|
||||
public static void prepareEnvironment(Map<String, String> environment,
|
||||
HgRepositoryHandler handler, HgHookManager hookManager)
|
||||
{
|
||||
prepareEnvironment(environment, handler, hookManager, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -105,65 +108,20 @@ public final class HgEnvironment
|
||||
if (request != null)
|
||||
{
|
||||
hookUrl = hookManager.createUrl(request);
|
||||
environment.put(SCM_CREDENTIALS, getCredentials(request));
|
||||
}
|
||||
else
|
||||
{
|
||||
hookUrl = hookManager.createUrl();
|
||||
}
|
||||
|
||||
try {
|
||||
String credentials = hookManager.getCredentials();
|
||||
environment.put(SCM_BEARER_TOKEN, credentials);
|
||||
} catch (ProvisionException e) {
|
||||
LOG.debug("could not create bearer token; looks like currently we are not in a request", e);
|
||||
}
|
||||
environment.put(ENV_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig()));
|
||||
environment.put(ENV_URL, hookUrl);
|
||||
environment.put(ENV_CHALLENGE, hookManager.getChallenge());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param environment
|
||||
* @param handler
|
||||
* @param hookManager
|
||||
*/
|
||||
public static void prepareEnvironment(Map<String, String> environment,
|
||||
HgRepositoryHandler handler, HgHookManager hookManager)
|
||||
{
|
||||
prepareEnvironment(environment, handler, hookManager, null);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static String getCredentials(HttpServletRequest request)
|
||||
{
|
||||
String credentials = null;
|
||||
String header = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
|
||||
|
||||
if (!Strings.isNullOrEmpty(header))
|
||||
{
|
||||
String[] parts = header.split("\\s+");
|
||||
|
||||
if (parts.length > 0)
|
||||
{
|
||||
CipherUtil cu = CipherUtil.getInstance();
|
||||
|
||||
credentials = cu.encode(Base64.decodeToString(parts[1]));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("invalid basic authentication header");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("could not find authentication header on request");
|
||||
}
|
||||
|
||||
return Strings.nullToEmpty(credentials);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.config.ScmConfigurationChangedEvent;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
@@ -78,19 +81,20 @@ public class HgHookManager
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param configuration
|
||||
* @param configuration
|
||||
* @param httpServletRequestProvider
|
||||
* @param httpClient
|
||||
* @param accessTokenBuilderFactory
|
||||
*/
|
||||
@Inject
|
||||
public HgHookManager(ScmConfiguration configuration,
|
||||
Provider<HttpServletRequest> httpServletRequestProvider,
|
||||
AdvancedHttpClient httpClient)
|
||||
Provider<HttpServletRequest> httpServletRequestProvider,
|
||||
AdvancedHttpClient httpClient, AccessTokenBuilderFactory accessTokenBuilderFactory)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.httpServletRequestProvider = httpServletRequestProvider;
|
||||
this.httpClient = httpClient;
|
||||
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -192,6 +196,13 @@ public class HgHookManager
|
||||
return this.challenge.equals(challenge);
|
||||
}
|
||||
|
||||
public String getCredentials()
|
||||
{
|
||||
AccessToken accessToken = accessTokenBuilderFactory.create().build();
|
||||
|
||||
return CipherUtil.getInstance().encode(accessToken.compact());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -391,4 +402,6 @@ public class HgHookManager
|
||||
|
||||
/** Field description */
|
||||
private Provider<HttpServletRequest> httpServletRequestProvider;
|
||||
|
||||
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import sonia.scm.io.INISection;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.repository.spi.HgRepositoryServiceProvider;
|
||||
import sonia.scm.repository.spi.HgWorkdirFactory;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.SystemUtil;
|
||||
@@ -113,10 +114,11 @@ public class HgRepositoryHandler
|
||||
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
|
||||
Provider<HgContext> hgContextProvider,
|
||||
RepositoryLocationResolver repositoryLocationResolver,
|
||||
PluginLoader pluginLoader)
|
||||
PluginLoader pluginLoader, HgWorkdirFactory workdirFactory)
|
||||
{
|
||||
super(storeFactory, repositoryLocationResolver, pluginLoader);
|
||||
this.hgContextProvider = hgContextProvider;
|
||||
this.workdirFactory = workdirFactory;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -408,6 +410,10 @@ public class HgRepositoryHandler
|
||||
}
|
||||
}
|
||||
|
||||
public HgWorkdirFactory getWorkdirFactory() {
|
||||
return workdirFactory;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
@@ -415,4 +421,6 @@ public class HgRepositoryHandler
|
||||
|
||||
/** Field description */
|
||||
private JAXBContext jaxbContext;
|
||||
|
||||
private final HgWorkdirFactory workdirFactory;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Changeset;
|
||||
import com.aragost.javahg.commands.CommitCommand;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import com.aragost.javahg.commands.UpdateCommand;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Mercurial implementation of the {@link BranchCommand}.
|
||||
* Note that this creates an empty commit to "persist" the new branch.
|
||||
*/
|
||||
public class HgBranchCommand extends AbstractCommand implements BranchCommand {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class);
|
||||
|
||||
private final HgWorkdirFactory workdirFactory;
|
||||
|
||||
HgBranchCommand(HgCommandContext context, Repository repository, HgWorkdirFactory workdirFactory) {
|
||||
super(context, repository);
|
||||
this.workdirFactory = workdirFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Branch branch(BranchRequest request) {
|
||||
try (WorkingCopy<com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext())) {
|
||||
com.aragost.javahg.Repository repository = workingCopy.getWorkingRepository();
|
||||
|
||||
checkoutParentBranchIfSpecified(request, repository);
|
||||
|
||||
Changeset emptyChangeset = createNewBranchWithEmptyCommit(request, repository);
|
||||
|
||||
LOG.debug("Created new branch '{}' in repository {} with changeset {}",
|
||||
request.getNewBranch(), getRepository().getNamespaceAndName(), emptyChangeset.getNode());
|
||||
|
||||
pullNewBranchIntoCentralRepository(request, workingCopy);
|
||||
|
||||
return Branch.normalBranch(request.getNewBranch(), emptyChangeset.getNode());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkoutParentBranchIfSpecified(BranchRequest request, com.aragost.javahg.Repository repository) {
|
||||
if (request.getParentBranch() != null) {
|
||||
try {
|
||||
UpdateCommand.on(repository).rev(request.getParentBranch()).execute();
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(getRepository(), "Could not check out parent branch " + request.getParentBranch(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Changeset createNewBranchWithEmptyCommit(BranchRequest request, com.aragost.javahg.Repository repository) {
|
||||
com.aragost.javahg.commands.BranchCommand.on(repository).set(request.getNewBranch());
|
||||
User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
|
||||
return CommitCommand
|
||||
.on(repository)
|
||||
.user(String.format("%s <%s>", currentUser.getDisplayName(), currentUser.getMail()))
|
||||
.message("Create new branch " + request.getNewBranch())
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void pullNewBranchIntoCentralRepository(BranchRequest request, WorkingCopy<com.aragost.javahg.Repository> workingCopy) {
|
||||
try {
|
||||
PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
||||
workdirFactory.configure(pullCommand);
|
||||
pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
// TODO handle failed update
|
||||
throw new IntegrateChangesFromWorkdirException(getRepository(),
|
||||
String.format("Could not pull new branch '%s' into central repository", request.getNewBranch()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ public class HgBranchesCommand extends AbstractCommand
|
||||
implements BranchesCommand
|
||||
{
|
||||
|
||||
private static final String DEFAULT_BRANCH_NAME = "default";
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
@@ -88,7 +90,11 @@ public class HgBranchesCommand extends AbstractCommand
|
||||
node = changeset.getNode();
|
||||
}
|
||||
|
||||
return new Branch(hgBranch.getName(), node);
|
||||
if (DEFAULT_BRANCH_NAME.equals(hgBranch.getName())) {
|
||||
return Branch.defaultBranch(hgBranch.getName(), node);
|
||||
} else {
|
||||
return Branch.normalBranch(hgBranch.getName(), node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ import sonia.scm.web.HgUtil;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -66,15 +68,14 @@ public class HgCommandContext implements Closeable
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param hookManager
|
||||
* @param handler
|
||||
* @param repository
|
||||
* @param directory
|
||||
*/
|
||||
public HgCommandContext(HgHookManager hookManager,
|
||||
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
|
||||
File directory)
|
||||
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
|
||||
File directory)
|
||||
{
|
||||
this(hookManager, handler, repository, directory,
|
||||
handler.getHgContext().isPending());
|
||||
@@ -84,26 +85,26 @@ public class HgCommandContext implements Closeable
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param hookManager
|
||||
* @param hanlder
|
||||
* @param handler
|
||||
* @param repository
|
||||
* @param directory
|
||||
* @param pending
|
||||
*/
|
||||
public HgCommandContext(HgHookManager hookManager,
|
||||
HgRepositoryHandler hanlder, sonia.scm.repository.Repository repository,
|
||||
File directory, boolean pending)
|
||||
HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
|
||||
File directory, boolean pending)
|
||||
{
|
||||
this.hookManager = hookManager;
|
||||
this.hanlder = hanlder;
|
||||
this.handler = handler;
|
||||
this.directory = directory;
|
||||
this.scmRepository = repository;
|
||||
this.encoding = repository.getProperty(PROPERTY_ENCODING);
|
||||
this.pending = pending;
|
||||
|
||||
if (Strings.isNullOrEmpty(encoding))
|
||||
{
|
||||
encoding = hanlder.getConfig().getEncoding();
|
||||
encoding = handler.getConfig().getEncoding();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,13 +135,18 @@ public class HgCommandContext implements Closeable
|
||||
{
|
||||
if (repository == null)
|
||||
{
|
||||
repository = HgUtil.open(hanlder, hookManager, directory, encoding,
|
||||
pending);
|
||||
repository = HgUtil.open(handler, hookManager, directory, encoding, pending);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
public Repository openWithSpecialEnvironment(BiConsumer<sonia.scm.repository.Repository, Map<String, String>> prepareEnvironment)
|
||||
{
|
||||
return HgUtil.open(handler, directory, encoding,
|
||||
pending, environment -> prepareEnvironment.accept(scmRepository, environment));
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -151,7 +157,11 @@ public class HgCommandContext implements Closeable
|
||||
*/
|
||||
public HgConfig getConfig()
|
||||
{
|
||||
return hanlder.getConfig();
|
||||
return handler.getConfig();
|
||||
}
|
||||
|
||||
public sonia.scm.repository.Repository getScmRepository() {
|
||||
return scmRepository;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
@@ -163,7 +173,7 @@ public class HgCommandContext implements Closeable
|
||||
private String encoding;
|
||||
|
||||
/** Field description */
|
||||
private HgRepositoryHandler hanlder;
|
||||
private HgRepositoryHandler handler;
|
||||
|
||||
/** Field description */
|
||||
private HgHookManager hookManager;
|
||||
@@ -173,4 +183,6 @@ public class HgCommandContext implements Closeable
|
||||
|
||||
/** Field description */
|
||||
private Repository repository;
|
||||
|
||||
private final sonia.scm.repository.Repository scmRepository;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
return new HgBranchesCommand(context, repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BranchCommand getBranchCommand() {
|
||||
return new HgBranchCommand(context, repository, handler.getWorkdirFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -192,6 +197,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
* @return the corresponding {@link ModificationsCommand} implemented from the Plugins
|
||||
* @throws CommandNotSupportedException if there is no Implementation
|
||||
*/
|
||||
@Override
|
||||
public ModificationsCommand getModificationsCommand() {
|
||||
return new HgModificationsCommand(context,repository);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import sonia.scm.repository.util.WorkdirFactory;
|
||||
|
||||
public interface HgWorkdirFactory extends WorkdirFactory<Repository, HgCommandContext> {
|
||||
void configure(PullCommand pullCommand);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.commands.CloneCommand;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import sonia.scm.repository.util.SimpleWorkdirFactory;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<Repository, HgCommandContext> implements HgWorkdirFactory {
|
||||
|
||||
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
|
||||
|
||||
@Inject
|
||||
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder) {
|
||||
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
|
||||
}
|
||||
@Override
|
||||
public ParentAndClone<Repository> cloneRepository(HgCommandContext context, File target) throws IOException {
|
||||
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer =
|
||||
(repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
|
||||
Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer);
|
||||
CloneCommand.on(centralRepository).execute(target.getAbsolutePath());
|
||||
return new ParentAndClone<>(centralRepository, Repository.open(target));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeRepository(Repository repository) {
|
||||
repository.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected sonia.scm.repository.Repository getScmRepository(HgCommandContext context) {
|
||||
return context.getScmRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(PullCommand pullCommand) {
|
||||
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.postHook");
|
||||
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.preHook");
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import org.slf4j.Logger;
|
||||
@@ -42,16 +41,12 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.HgConfig;
|
||||
import sonia.scm.repository.HgEnvironment;
|
||||
import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgPythonScript;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryRequestListenerUtil;
|
||||
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.util.AssertUtil;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.cgi.CGIExecutor;
|
||||
import sonia.scm.web.cgi.CGIExecutorFactory;
|
||||
import sonia.scm.web.cgi.EnvList;
|
||||
@@ -62,14 +57,12 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -79,25 +72,9 @@ import java.util.Base64;
|
||||
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
{
|
||||
|
||||
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_REPOSITORY_NAME = "REPO_NAME";
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
|
||||
|
||||
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
|
||||
|
||||
/** Field description */
|
||||
public static final String ENV_SESSION_PREFIX = "SCM_";
|
||||
|
||||
/** Field description */
|
||||
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = -3492811300905099810L;
|
||||
|
||||
@@ -107,30 +84,18 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param cgiExecutorFactory
|
||||
* @param configuration
|
||||
* @param handler
|
||||
* @param hookManager
|
||||
* @param requestListenerUtil
|
||||
*/
|
||||
@Inject
|
||||
public HgCGIServlet(CGIExecutorFactory cgiExecutorFactory,
|
||||
ScmConfiguration configuration,
|
||||
HgRepositoryHandler handler, HgHookManager hookManager,
|
||||
RepositoryRequestListenerUtil requestListenerUtil)
|
||||
ScmConfiguration configuration,
|
||||
HgRepositoryHandler handler,
|
||||
RepositoryRequestListenerUtil requestListenerUtil,
|
||||
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder)
|
||||
{
|
||||
this.cgiExecutorFactory = cgiExecutorFactory;
|
||||
this.configuration = configuration;
|
||||
this.handler = handler;
|
||||
this.hookManager = hookManager;
|
||||
this.requestListenerUtil = requestListenerUtil;
|
||||
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
|
||||
this.exceptionHandler = new HgCGIExceptionHandler();
|
||||
this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext());
|
||||
}
|
||||
@@ -163,40 +128,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param env
|
||||
* @param request
|
||||
*/
|
||||
private void addCredentials(EnvList env, HttpServletRequest request)
|
||||
{
|
||||
String authorization = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
|
||||
|
||||
if (!Strings.isNullOrEmpty(authorization))
|
||||
{
|
||||
if (authorization.startsWith(HttpUtil.AUTHORIZATION_SCHEME_BASIC))
|
||||
{
|
||||
String encodedUserInfo =
|
||||
authorization.substring(
|
||||
HttpUtil.AUTHORIZATION_SCHEME_BASIC.length()).trim();
|
||||
// TODO check encoding of user-agent ?
|
||||
String userInfo = new String(Base64.getDecoder().decode(encodedUserInfo));
|
||||
|
||||
env.set(SCM_CREDENTIALS, CipherUtil.getInstance().encode(userInfo));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("unknow authentication scheme used");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.trace("no authorization header found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -262,7 +193,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
HttpServletResponse response, Repository repository)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
File directory = handler.getDirectory(repository.getId());
|
||||
CGIExecutor executor = cgiExecutorFactory.createExecutor(configuration,
|
||||
getServletContext(), request, response);
|
||||
|
||||
@@ -271,41 +201,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
executor.setExceptionHandler(exceptionHandler);
|
||||
executor.setStatusCodeHandler(exceptionHandler);
|
||||
executor.setContentLengthWorkaround(true);
|
||||
executor.getEnvironment().set(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
|
||||
executor.getEnvironment().set(ENV_REPOSITORY_ID, repository.getId());
|
||||
executor.getEnvironment().set(ENV_REPOSITORY_PATH,
|
||||
directory.getAbsolutePath());
|
||||
|
||||
// add hook environment
|
||||
Map<String, String> environment = executor.getEnvironment().asMutableMap();
|
||||
if (handler.getConfig().isDisableHookSSLValidation()) {
|
||||
// disable ssl validation
|
||||
// Issue 959: https://goo.gl/zH5eY8
|
||||
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
|
||||
}
|
||||
|
||||
// enable experimental httppostargs protocol of mercurial
|
||||
// Issue 970: https://goo.gl/poascp
|
||||
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
|
||||
|
||||
//J-
|
||||
HgEnvironment.prepareEnvironment(
|
||||
environment,
|
||||
handler,
|
||||
hookManager,
|
||||
request
|
||||
);
|
||||
//J+
|
||||
|
||||
addCredentials(executor.getEnvironment(), request);
|
||||
|
||||
// unused ???
|
||||
HttpSession session = request.getSession(false);
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
passSessionAttributes(executor.getEnvironment(), session);
|
||||
}
|
||||
hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap());
|
||||
|
||||
String interpreter = getInterpreter();
|
||||
|
||||
@@ -358,9 +254,8 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
|
||||
/** Field description */
|
||||
private final HgRepositoryHandler handler;
|
||||
|
||||
/** Field description */
|
||||
private final HgHookManager hookManager;
|
||||
|
||||
/** Field description */
|
||||
private final RepositoryRequestListenerUtil requestListenerUtil;
|
||||
|
||||
private final HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -54,8 +55,8 @@ import sonia.scm.repository.api.HgHookMessage;
|
||||
import sonia.scm.repository.api.HgHookMessage.Severity;
|
||||
import sonia.scm.repository.spi.HgHookContextProvider;
|
||||
import sonia.scm.repository.spi.HookEventFacade;
|
||||
import sonia.scm.security.BearerToken;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.security.Tokens;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
@@ -93,7 +94,7 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
private static final String PARAM_CHALLENGE = "challenge";
|
||||
|
||||
/** Field description */
|
||||
private static final String PARAM_CREDENTIALS = "credentials";
|
||||
private static final String PARAM_TOKEN = "token";
|
||||
|
||||
/** Field description */
|
||||
private static final String PARAM_NODE = "node";
|
||||
@@ -179,11 +180,11 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
|
||||
if (Util.isNotEmpty(node))
|
||||
{
|
||||
String credentials = request.getParameter(PARAM_CREDENTIALS);
|
||||
String token = request.getParameter(PARAM_TOKEN);
|
||||
|
||||
if (Util.isNotEmpty(credentials))
|
||||
if (Util.isNotEmpty(token))
|
||||
{
|
||||
authenticate(request, credentials);
|
||||
authenticate(token);
|
||||
}
|
||||
|
||||
hookCallback(response, type, repositoryId, challenge, node);
|
||||
@@ -209,34 +210,20 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
}
|
||||
}
|
||||
|
||||
private void authenticate(HttpServletRequest request, String credentials)
|
||||
private void authenticate(String token)
|
||||
{
|
||||
try
|
||||
{
|
||||
credentials = CipherUtil.getInstance().decode(credentials);
|
||||
token = CipherUtil.getInstance().decode(token);
|
||||
|
||||
if (Util.isNotEmpty(credentials))
|
||||
if (Util.isNotEmpty(token))
|
||||
{
|
||||
int index = credentials.indexOf(':');
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
if (index > 0 && index < credentials.length())
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
AuthenticationToken accessToken = createToken(token);
|
||||
|
||||
//J-
|
||||
subject.login(
|
||||
Tokens.createAuthenticationToken(
|
||||
request,
|
||||
credentials.substring(0, index),
|
||||
credentials.substring(index + 1)
|
||||
)
|
||||
);
|
||||
//J+
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error("could not find delimiter");
|
||||
}
|
||||
//J-
|
||||
subject.login(accessToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -245,6 +232,11 @@ public class HgHookCallbackServlet extends HttpServlet
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationToken createToken(String tokenString)
|
||||
{
|
||||
return BearerToken.valueOf(tokenString);
|
||||
}
|
||||
|
||||
private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type)
|
||||
throws IOException
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import sonia.scm.repository.HgEnvironment;
|
||||
import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class HgRepositoryEnvironmentBuilder {
|
||||
|
||||
private static final String ENV_REPOSITORY_NAME = "REPO_NAME";
|
||||
private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
|
||||
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
|
||||
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
|
||||
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
|
||||
|
||||
private final HgRepositoryHandler handler;
|
||||
private final HgHookManager hookManager;
|
||||
|
||||
@Inject
|
||||
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) {
|
||||
this.handler = handler;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) {
|
||||
File directory = handler.getDirectory(repository.getId());
|
||||
|
||||
environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
|
||||
environment.put(ENV_REPOSITORY_ID, repository.getId());
|
||||
environment.put(ENV_REPOSITORY_PATH,
|
||||
directory.getAbsolutePath());
|
||||
|
||||
// add hook environment
|
||||
if (handler.getConfig().isDisableHookSSLValidation()) {
|
||||
// disable ssl validation
|
||||
// Issue 959: https://goo.gl/zH5eY8
|
||||
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
|
||||
}
|
||||
|
||||
// enable experimental httppostargs protocol of mercurial
|
||||
// Issue 970: https://goo.gl/poascp
|
||||
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
|
||||
|
||||
HgEnvironment.prepareEnvironment(
|
||||
environment,
|
||||
handler,
|
||||
hookManager,
|
||||
request
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.HgContext;
|
||||
import sonia.scm.repository.HgContextProvider;
|
||||
import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.spi.HgWorkdirFactory;
|
||||
import sonia.scm.repository.spi.SimpleHgWorkdirFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -81,5 +83,7 @@ public class HgServletModule extends ServletModule
|
||||
|
||||
// bind servlets
|
||||
serve(MAPPING_HOOK).with(HgHookCallbackServlet.class);
|
||||
|
||||
bind(HgWorkdirFactory.class).to(SimpleHgWorkdirFactory.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ import sonia.scm.util.Util;
|
||||
import java.io.File;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -103,6 +105,19 @@ public final class HgUtil
|
||||
*/
|
||||
public static Repository open(HgRepositoryHandler handler,
|
||||
HgHookManager hookManager, File directory, String encoding, boolean pending)
|
||||
{
|
||||
return open(
|
||||
handler,
|
||||
directory,
|
||||
encoding,
|
||||
pending,
|
||||
environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager)
|
||||
);
|
||||
}
|
||||
|
||||
public static Repository open(HgRepositoryHandler handler,
|
||||
File directory, String encoding, boolean pending,
|
||||
Consumer<Map<String, String>> prepareEnvironment)
|
||||
{
|
||||
String enc = encoding;
|
||||
|
||||
@@ -113,8 +128,7 @@ public final class HgUtil
|
||||
|
||||
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
|
||||
|
||||
HgEnvironment.prepareEnvironment(repoConfiguration.getEnvironment(),
|
||||
handler, hookManager);
|
||||
prepareEnvironment.accept(repoConfiguration.getEnvironment());
|
||||
|
||||
repoConfiguration.addExtension(HgFileviewExtension.class);
|
||||
repoConfiguration.setEnablePendingChangesets(pending);
|
||||
|
||||
@@ -40,7 +40,7 @@ import os, urllib, urllib2
|
||||
|
||||
baseUrl = os.environ['SCM_URL']
|
||||
challenge = os.environ['SCM_CHALLENGE']
|
||||
credentials = os.environ['SCM_CREDENTIALS']
|
||||
token = os.environ['SCM_BEARER_TOKEN']
|
||||
repositoryId = os.environ['SCM_REPOSITORY_ID']
|
||||
|
||||
def printMessages(ui, msgs):
|
||||
@@ -54,13 +54,13 @@ def callHookUrl(ui, repo, hooktype, node):
|
||||
try:
|
||||
url = baseUrl + hooktype
|
||||
ui.debug( "send scm-hook to " + url + " and " + node + "\n" )
|
||||
data = urllib.urlencode({'node': node, 'challenge': challenge, 'credentials': credentials, 'repositoryPath': repo.root, 'repositoryId': repositoryId})
|
||||
data = urllib.urlencode({'node': node, 'challenge': challenge, 'token': token, 'repositoryPath': repo.root, 'repositoryId': repositoryId})
|
||||
# open url but ignore proxy settings
|
||||
proxy_handler = urllib2.ProxyHandler({})
|
||||
opener = urllib2.build_opener(proxy_handler)
|
||||
req = urllib2.Request(url, data)
|
||||
conn = opener.open(req)
|
||||
if conn.code >= 200 and conn.code < 300:
|
||||
if 200 <= conn.code < 300:
|
||||
ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" )
|
||||
printMessages(ui, conn)
|
||||
abort = False
|
||||
|
||||
@@ -77,7 +77,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
@Override
|
||||
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
|
||||
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null);
|
||||
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null, null);
|
||||
|
||||
handler.init(contextProvider);
|
||||
HgTestUtil.checkForSkip(handler);
|
||||
@@ -87,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
||||
|
||||
@Test
|
||||
public void getDirectory() {
|
||||
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null);
|
||||
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null, null);
|
||||
|
||||
HgConfig hgConfig = new HgConfig();
|
||||
hgConfig.setHgBinary("hg");
|
||||
|
||||
@@ -105,7 +105,7 @@ public final class HgTestUtil
|
||||
|
||||
RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver());
|
||||
HgRepositoryHandler handler =
|
||||
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null);
|
||||
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null);
|
||||
Path repoDir = directory.toPath();
|
||||
when(repoDao.getPath(any())).thenReturn(repoDir);
|
||||
handler.init(context);
|
||||
@@ -128,6 +128,7 @@ public final class HgTestUtil
|
||||
"http://localhost:8081/scm/hook/hg/");
|
||||
when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn(
|
||||
"http://localhost:8081/scm/hook/hg/");
|
||||
when(hookManager.getCredentials()).thenReturn("");
|
||||
|
||||
return hookManager;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class HgBranchCommand implements BranchCommand
|
||||
public Branch branch(String name) throws IOException
|
||||
{
|
||||
com.aragost.javahg.commands.BranchCommand.on(repository).set(name);
|
||||
return new Branch(name, repository.tip().getNode());
|
||||
return Branch.normalBranch(name, repository.tip().getNode());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.junit.Before;
|
||||
|
||||
import sonia.scm.repository.HgRepositoryHandler;
|
||||
import sonia.scm.repository.HgTestUtil;
|
||||
import sonia.scm.repository.RepositoryPathNotFoundException;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.util.MockUtil;
|
||||
|
||||
@@ -77,7 +76,7 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase
|
||||
* @throws IOException
|
||||
*/
|
||||
@Before
|
||||
public void initHgHandler() throws IOException, RepositoryPathNotFoundException {
|
||||
public void initHgHandler() throws IOException {
|
||||
this.handler = HgTestUtil.createHandler(tempFolder.newFolder());
|
||||
|
||||
HgTestUtil.checkForSkip(handler);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.HgTestUtil;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class HgBranchCommandTest extends AbstractHgCommandTestBase {
|
||||
@Test
|
||||
public void shouldCreateBranch() throws IOException {
|
||||
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
|
||||
|
||||
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
|
||||
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
|
||||
|
||||
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder)) {
|
||||
@Override
|
||||
public void configure(PullCommand pullCommand) {
|
||||
// we do not want to configure http hooks in this unit test
|
||||
}
|
||||
};
|
||||
|
||||
BranchRequest branchRequest = new BranchRequest();
|
||||
branchRequest.setNewBranch("new_branch");
|
||||
|
||||
new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest);
|
||||
|
||||
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
|
||||
}
|
||||
|
||||
private List<Branch> readBranches() {
|
||||
return new HgBranchesCommand(cmdContext, repository).getBranches();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { AsyncCreatable } from "react-select";
|
||||
import { AsyncCreatable, Async } from "react-select";
|
||||
import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
|
||||
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
|
||||
|
||||
@@ -13,7 +13,8 @@ type Props = {
|
||||
value?: SelectValue,
|
||||
placeholder: string,
|
||||
loadingMessage: string,
|
||||
noOptionsMessage: string
|
||||
noOptionsMessage: string,
|
||||
creatable?: boolean
|
||||
};
|
||||
|
||||
|
||||
@@ -42,27 +43,40 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions } = this.props;
|
||||
const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<AsyncCreatable
|
||||
cacheOptions
|
||||
loadOptions={loadSuggestions}
|
||||
onChange={this.handleInputChange}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
loadingMessage={() => loadingMessage}
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
isValidNewOption={this.isValidNewOption}
|
||||
onCreateOption={value => {
|
||||
this.handleInputChange({
|
||||
label: value,
|
||||
value: { id: value, displayName: value }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{creatable?
|
||||
<AsyncCreatable
|
||||
cacheOptions
|
||||
loadOptions={loadSuggestions}
|
||||
onChange={this.handleInputChange}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
loadingMessage={() => loadingMessage}
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
isValidNewOption={this.isValidNewOption}
|
||||
onCreateOption={value => {
|
||||
this.handleInputChange({
|
||||
label: value,
|
||||
value: { id: value, displayName: value }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
:
|
||||
<Async
|
||||
cacheOptions
|
||||
loadOptions={loadSuggestions}
|
||||
onChange={this.handleInputChange}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
loadingMessage={() => loadingMessage}
|
||||
noOptionsMessage={() => noOptionsMessage}
|
||||
/>
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,15 +19,6 @@ class BaseUrlSettings extends React.Component<Props> {
|
||||
<div>
|
||||
<Subtitle subtitle={t("base-url-settings.name")} />
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
checked={forceBaseUrl}
|
||||
label={t("base-url-settings.force-base-url")}
|
||||
onChange={this.handleForceBaseUrlChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.forceBaseUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("base-url-settings.base-url")}
|
||||
@@ -37,6 +28,15 @@ class BaseUrlSettings extends React.Component<Props> {
|
||||
helpText={t("help.baseUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
checked={forceBaseUrl}
|
||||
label={t("base-url-settings.force-base-url")}
|
||||
onChange={this.handleForceBaseUrlChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.forceBaseUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package sonia.scm;
|
||||
|
||||
import sonia.scm.search.SearchRequest;
|
||||
import sonia.scm.search.SearchUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static sonia.scm.group.DisplayGroup.from;
|
||||
|
||||
public abstract class GenericDisplayManager<D, T extends ReducedModelObject> implements DisplayManager<T> {
|
||||
|
||||
private final GenericDAO<D> dao;
|
||||
private final Function<D, T> transform;
|
||||
|
||||
protected GenericDisplayManager(GenericDAO<D> dao, Function<D, T> transform) {
|
||||
this.dao = dao;
|
||||
this.transform = transform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> autocomplete(String filter) {
|
||||
checkPermission();
|
||||
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
|
||||
return SearchUtil.search(
|
||||
searchRequest,
|
||||
dao.getAll(),
|
||||
object -> matches(searchRequest, object)? transform.apply(object): null
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract void checkPermission();
|
||||
|
||||
protected abstract boolean matches(SearchRequest searchRequest, D object);
|
||||
|
||||
@Override
|
||||
public Optional<T> get(String id) {
|
||||
return ofNullable(dao.get(id)).map(transform);
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,10 @@ import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.cache.GuavaCacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.group.DefaultGroupDisplayManager;
|
||||
import sonia.scm.group.DefaultGroupManager;
|
||||
import sonia.scm.group.GroupDAO;
|
||||
import sonia.scm.group.GroupDisplayManager;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.GroupManagerProvider;
|
||||
import sonia.scm.group.xml.XmlGroupDAO;
|
||||
@@ -102,8 +104,10 @@ import sonia.scm.template.MustacheTemplateEngine;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
import sonia.scm.template.TemplateServlet;
|
||||
import sonia.scm.user.DefaultUserDisplayManager;
|
||||
import sonia.scm.user.DefaultUserManager;
|
||||
import sonia.scm.user.UserDAO;
|
||||
import sonia.scm.user.UserDisplayManager;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.UserManagerProvider;
|
||||
import sonia.scm.user.xml.XmlUserDAO;
|
||||
@@ -268,8 +272,11 @@ public class ScmServletModule extends ServletModule
|
||||
RepositoryManagerProvider.class);
|
||||
bindDecorated(UserManager.class, DefaultUserManager.class,
|
||||
UserManagerProvider.class);
|
||||
bind(UserDisplayManager.class, DefaultUserDisplayManager.class);
|
||||
bindDecorated(GroupManager.class, DefaultGroupManager.class,
|
||||
GroupManagerProvider.class);
|
||||
bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class);
|
||||
|
||||
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);
|
||||
|
||||
// bind sslcontext provider
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package sonia.scm.api;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
import sonia.scm.api.v2.resources.ErrorDto;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class ContextualFallbackExceptionMapper implements ExceptionMapper<ExceptionWithContext> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ContextualFallbackExceptionMapper.class);
|
||||
|
||||
@Override
|
||||
public Response toResponse(ExceptionWithContext exception) {
|
||||
logger.warn("mapping unexpected {} to status code 500", exception.getClass().getName(), exception);
|
||||
ErrorDto errorDto = new ErrorDto();
|
||||
errorDto.setMessage(exception.getMessage());
|
||||
errorDto.setContext(exception.getContext());
|
||||
errorDto.setErrorCode(exception.getCode());
|
||||
errorDto.setTransactionId(MDC.get("transaction_id"));
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(errorDto)
|
||||
.type(VndMediaType.ERROR_TYPE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import sonia.scm.ReducedModelObject;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.group.GroupDisplayManager;
|
||||
import sonia.scm.user.UserDisplayManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -30,14 +30,14 @@ public class AutoCompleteResource {
|
||||
|
||||
private ReducedObjectModelToDtoMapper mapper;
|
||||
|
||||
private UserManager userManager;
|
||||
private GroupManager groupManager;
|
||||
private UserDisplayManager userDisplayManager;
|
||||
private GroupDisplayManager groupDisplayManager;
|
||||
|
||||
@Inject
|
||||
public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserManager userManager, GroupManager groupManager) {
|
||||
public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserDisplayManager userDisplayManager, GroupDisplayManager groupDisplayManager) {
|
||||
this.mapper = mapper;
|
||||
this.userManager = userManager;
|
||||
this.groupManager = groupManager;
|
||||
this.userDisplayManager = userDisplayManager;
|
||||
this.groupDisplayManager = groupDisplayManager;
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -51,7 +51,7 @@ public class AutoCompleteResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public List<ReducedObjectModelDto> searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
|
||||
return map(userManager.autocomplete(filter));
|
||||
return map(userDisplayManager.autocomplete(filter));
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -65,7 +65,7 @@ public class AutoCompleteResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public List<ReducedObjectModelDto> searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
|
||||
return map(groupManager.autocomplete(filter));
|
||||
return map(groupDisplayManager.autocomplete(filter));
|
||||
}
|
||||
|
||||
private <T extends ReducedModelObject> List<ReducedObjectModelDto> map(Collection<T> autocomplete) {
|
||||
|
||||
@@ -3,9 +3,12 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.inject.Inject;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -25,22 +28,36 @@ public class BranchCollectionToDtoMapper {
|
||||
this.branchToDtoMapper = branchToDtoMapper;
|
||||
}
|
||||
|
||||
public HalRepresentation map(String namespace, String name, Collection<Branch> branches) {
|
||||
return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches)));
|
||||
public HalRepresentation map(Repository repository, Collection<Branch> branches) {
|
||||
return new HalRepresentation(
|
||||
createLinks(repository),
|
||||
embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches)));
|
||||
}
|
||||
|
||||
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) {
|
||||
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
|
||||
}
|
||||
|
||||
private Links createLinks(String namespace, String name) {
|
||||
private Links createLinks(Repository repository) {
|
||||
String namespace = repository.getNamespace();
|
||||
String name = repository.getName();
|
||||
String baseUrl = resourceLinks.branchCollection().self(namespace, name);
|
||||
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.with(Links.linkingTo().self(baseUrl).build());
|
||||
Links.Builder linksBuilder = linkingTo().with(createSelfLink(baseUrl));
|
||||
if (RepositoryPermissions.push(repository).isPermitted()) {
|
||||
linksBuilder.single(createCreateLink(namespace, name));
|
||||
}
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
private Links createSelfLink(String baseUrl) {
|
||||
return Links.linkingTo().self(baseUrl).build();
|
||||
}
|
||||
|
||||
private Link createCreateLink(String namespace, String name) {
|
||||
return Link.link("create", resourceLinks.branch().create(namespace, name));
|
||||
}
|
||||
|
||||
private Embedded embedDtos(List<BranchDto> dtos) {
|
||||
return embeddedBuilder()
|
||||
.with("branches", dtos)
|
||||
|
||||
@@ -6,12 +6,22 @@ import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
@Getter @Setter @NoArgsConstructor
|
||||
public class BranchDto extends HalRepresentation {
|
||||
|
||||
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
|
||||
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
|
||||
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
|
||||
|
||||
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
|
||||
private String name;
|
||||
private String revision;
|
||||
private boolean defaultBranch;
|
||||
|
||||
BranchDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
import static sonia.scm.api.v2.resources.BranchDto.VALID_BRANCH_NAMES;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BranchRequestDto {
|
||||
|
||||
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
|
||||
private String name;
|
||||
private String parent;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.Branches;
|
||||
@@ -12,21 +14,27 @@ 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.BranchCommandBuilder;
|
||||
import sonia.scm.repository.api.CommandNotSupportedException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import static sonia.scm.AlreadyExistsException.alreadyExists;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
@@ -38,12 +46,15 @@ public class BranchRootResource {
|
||||
|
||||
private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
|
||||
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper, ResourceLinks resourceLinks) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.branchToDtoMapper = branchToDtoMapper;
|
||||
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
|
||||
this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,12 +111,7 @@ public class BranchRootResource {
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
boolean branchExists = repositoryService.getBranchesCommand()
|
||||
.getBranches()
|
||||
.getBranches()
|
||||
.stream()
|
||||
.anyMatch(branch -> branchName.equals(branch.getName()));
|
||||
if (!branchExists){
|
||||
if (!branchExists(branchName, repositoryService)){
|
||||
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
|
||||
}
|
||||
Repository repository = repositoryService.getRepository();
|
||||
@@ -125,6 +131,58 @@ public class BranchRootResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new branch.
|
||||
*
|
||||
* @param namespace the namespace of the repository
|
||||
* @param name the name of the repository
|
||||
* @param branchRequest the request giving the name of the new branch and an optional parent branch
|
||||
* @return A response with the link to the new branch (if created successfully).
|
||||
*/
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.BRANCH_REQUEST)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"push\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch"))
|
||||
public Response create(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@Valid BranchRequestDto branchRequest) throws IOException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
String branchName = branchRequest.getName();
|
||||
String parentName = branchRequest.getParent();
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
if (branchExists(branchName, repositoryService)) {
|
||||
throw alreadyExists(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
|
||||
}
|
||||
Repository repository = repositoryService.getRepository();
|
||||
RepositoryPermissions.push(repository).check();
|
||||
BranchCommandBuilder branchCommand = repositoryService.getBranchCommand();
|
||||
if (!Strings.isNullOrEmpty(parentName)) {
|
||||
if (!branchExists(parentName, repositoryService)) {
|
||||
throw notFound(entity(Branch.class, parentName).in(Repository.class, namespace + "/" + name));
|
||||
}
|
||||
branchCommand.from(parentName);
|
||||
}
|
||||
Branch newBranch = branchCommand.branch(branchName);
|
||||
return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean branchExists(String branchName, RepositoryService repositoryService) throws IOException {
|
||||
return repositoryService.getBranchesCommand()
|
||||
.getBranches()
|
||||
.getBranches()
|
||||
.stream()
|
||||
.anyMatch(branch -> branchName.equals(branch.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the branches for a repository.
|
||||
*
|
||||
@@ -141,14 +199,14 @@ public class BranchRootResource {
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "branches not supported for given repository"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"read repository\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository found for the given namespace and name"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
|
||||
return Response.ok(branchCollectionToDtoMapper.map(repositoryService.getRepository(), branches.getBranches())).build();
|
||||
} catch (CommandNotSupportedException ex) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
||||
}
|
||||
if (repositoryService.isSupported(Command.BRANCHES)) {
|
||||
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name,
|
||||
getListOfObjects(source.getBranches(), branchName -> new Branch(branchName, source.getId()))));
|
||||
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
|
||||
}
|
||||
}
|
||||
embeddedBuilder.with("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository)));
|
||||
|
||||
@@ -386,6 +386,9 @@ class ResourceLinks {
|
||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
|
||||
}
|
||||
|
||||
public String create(String namespace, String name) {
|
||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public IncomingLinks incoming() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package sonia.scm.group;
|
||||
|
||||
import sonia.scm.GenericDisplayManager;
|
||||
import sonia.scm.search.SearchRequest;
|
||||
import sonia.scm.search.SearchUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class DefaultGroupDisplayManager extends GenericDisplayManager<Group, DisplayGroup> implements GroupDisplayManager {
|
||||
|
||||
@Inject
|
||||
public DefaultGroupDisplayManager(GroupDAO groupDAO) {
|
||||
super(groupDAO, DisplayGroup::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkPermission() {
|
||||
GroupPermissions.autocomplete().check();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matches(SearchRequest searchRequest, Group group) {
|
||||
return SearchUtil.matchesOne(searchRequest, group.getName(), group.getDescription());
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ public class DefaultGroupManager extends AbstractGroupManager
|
||||
|
||||
final PermissionActionCheck<Group> check = GroupPermissions.read();
|
||||
return SearchUtil.search(searchRequest, groupDAO.getAll(),
|
||||
new TransformFilter<Group>()
|
||||
new TransformFilter<Group, Group>()
|
||||
{
|
||||
@Override
|
||||
public Group accept(Group group)
|
||||
@@ -241,13 +241,6 @@ public class DefaultGroupManager extends AbstractGroupManager
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Group> autocomplete(String filter) {
|
||||
GroupPermissions.autocomplete().check();
|
||||
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
|
||||
return SearchUtil.search(searchRequest, groupDAO.getAll(), group -> matches(searchRequest,group)?group:null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package sonia.scm.user;
|
||||
|
||||
import sonia.scm.GenericDisplayManager;
|
||||
import sonia.scm.search.SearchRequest;
|
||||
import sonia.scm.search.SearchUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class DefaultUserDisplayManager extends GenericDisplayManager<User, DisplayUser> implements UserDisplayManager {
|
||||
|
||||
@Inject
|
||||
public DefaultUserDisplayManager(UserDAO userDAO) {
|
||||
super(userDAO, DisplayUser::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkPermission() {
|
||||
UserPermissions.autocomplete().check();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matches(SearchRequest searchRequest, User user) {
|
||||
return SearchUtil.matchesOne(searchRequest, user.getName(), user.getDisplayName(), user.getMail());
|
||||
}
|
||||
}
|
||||
@@ -212,13 +212,6 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
fresh.copyProperties(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<User> autocomplete(String filter) {
|
||||
UserPermissions.autocomplete().check();
|
||||
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
|
||||
return SearchUtil.search(searchRequest, userDAO.getAll(), user -> matches(searchRequest,user)?user:null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -236,7 +229,7 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
}
|
||||
|
||||
final PermissionActionCheck<User> check = UserPermissions.read();
|
||||
return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter<User>() {
|
||||
return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter<User, User>() {
|
||||
@Override
|
||||
public User accept(User user)
|
||||
{
|
||||
@@ -415,35 +408,6 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
this.modify(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param unmarshaller
|
||||
* @param path
|
||||
*/
|
||||
private void createDefaultAccount(Unmarshaller unmarshaller, String path)
|
||||
{
|
||||
InputStream input = DefaultUserManager.class.getResourceAsStream(path);
|
||||
|
||||
try
|
||||
{
|
||||
User user = (User) unmarshaller.unmarshal(input);
|
||||
|
||||
user.setType(userDAO.getType());
|
||||
user.setCreationDate(System.currentTimeMillis());
|
||||
userDAO.add(user);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.error("could not create account", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IOUtil.close(input);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
private final UserDAO userDAO;
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
"3zR9vPNIE1": {
|
||||
"displayName": "Ungültige Eingabe",
|
||||
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut."
|
||||
},
|
||||
"CHRM7IQzo1": {
|
||||
"displayName": "Änderung fehlgeschlagen",
|
||||
"description": "Die Änderung ist fehlgeschlagen. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
"3zR9vPNIE1": {
|
||||
"displayName": "Illegal input",
|
||||
"description": "The values could not be validated. Please correct your input and try again."
|
||||
},
|
||||
"CHRM7IQzo1": {
|
||||
"displayName": "Change failed",
|
||||
"description": "The change failed. Please contact your administrator for further assistance."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -14,16 +14,14 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.Manager;
|
||||
import sonia.scm.group.DefaultGroupManager;
|
||||
import sonia.scm.DisplayManager;
|
||||
import sonia.scm.group.DefaultGroupDisplayManager;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.xml.XmlGroupDAO;
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.user.DefaultUserManager;
|
||||
import sonia.scm.user.DefaultUserDisplayManager;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.user.xml.XmlUserDAO;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
import sonia.scm.xml.XmlDatabase;
|
||||
@@ -51,7 +49,7 @@ public class AutoCompleteResourceTest {
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
public static final String URL = "/" + AutoCompleteResource.PATH;
|
||||
private final Integer defaultLimit = Manager.DEFAULT_LIMIT;
|
||||
private final Integer defaultLimit = DisplayManager.DEFAULT_LIMIT;
|
||||
private Dispatcher dispatcher;
|
||||
|
||||
private XmlUserDAO userDao;
|
||||
@@ -73,8 +71,8 @@ public class AutoCompleteResourceTest {
|
||||
XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory);
|
||||
groupDao = spy(groupDAO);
|
||||
ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl();
|
||||
UserManager userManager = new DefaultUserManager(this.userDao);
|
||||
GroupManager groupManager = new DefaultGroupManager(groupDao);
|
||||
DefaultUserDisplayManager userManager = new DefaultUserDisplayManager(this.userDao);
|
||||
DefaultGroupDisplayManager groupManager = new DefaultGroupDisplayManager(groupDao);
|
||||
AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager);
|
||||
dispatcher = createDispatcher(autoCompleteResource);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BranchDtoTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"v",
|
||||
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||
"val#x",
|
||||
"val&x",
|
||||
"val+",
|
||||
"val,kill",
|
||||
"val.kill",
|
||||
"val;kill",
|
||||
"val<kill",
|
||||
"val=",
|
||||
"val>kill",
|
||||
"val@",
|
||||
"val]id",
|
||||
"val`id",
|
||||
"valid#",
|
||||
"valid.t",
|
||||
"val{",
|
||||
"val{d",
|
||||
"val{}d",
|
||||
"val|kill",
|
||||
"val}"
|
||||
})
|
||||
void shouldAcceptValidBranchName(String branchName) {
|
||||
assertTrue(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"",
|
||||
".val",
|
||||
"val.",
|
||||
"/val",
|
||||
"val/",
|
||||
"val id"
|
||||
})
|
||||
void shouldRejectInvalidBranchName(String branchName) {
|
||||
assertFalse(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,12 @@ 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.BranchCommandBuilder;
|
||||
import sonia.scm.repository.api.BranchesCommandBuilder;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
@@ -41,6 +43,8 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
@@ -49,7 +53,8 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
public static final String BRANCH_PATH = "space/repo/branches/master";
|
||||
public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH;
|
||||
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
public static final String REVISION = "revision";
|
||||
private Dispatcher dispatcher;
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
|
||||
@@ -60,6 +65,8 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
private RepositoryService service;
|
||||
@Mock
|
||||
private BranchesCommandBuilder branchesCommandBuilder;
|
||||
@Mock
|
||||
private BranchCommandBuilder branchCommandBuilder;
|
||||
|
||||
@Mock
|
||||
private LogCommandBuilder logCommandBuilder;
|
||||
@@ -89,17 +96,18 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() throws Exception {
|
||||
public void prepareEnvironment() {
|
||||
changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
|
||||
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper);
|
||||
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper, resourceLinks);
|
||||
super.branchRootResource = Providers.of(branchRootResource);
|
||||
dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
|
||||
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
|
||||
when(service.getBranchesCommand()).thenReturn(branchesCommandBuilder);
|
||||
when(service.getBranchCommand()).thenReturn(branchCommandBuilder);
|
||||
when(service.getLogCommand()).thenReturn(logCommandBuilder);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
@@ -125,7 +133,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldFindExistingBranch() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(new Branch("master", "revision")));
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("master")));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get(BRANCH_URL);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
@@ -139,13 +147,12 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldFindHistory() throws Exception {
|
||||
String id = "revision_123";
|
||||
Instant creationDate = Instant.now();
|
||||
String authorName = "name";
|
||||
String authorEmail = "em@i.l";
|
||||
String commit = "my branch commit";
|
||||
ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class);
|
||||
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
|
||||
List<Changeset> changesetList = Lists.newArrayList(new Changeset(REVISION, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
|
||||
when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
|
||||
when(changesetPagingResult.getTotal()).thenReturn(1);
|
||||
when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder);
|
||||
@@ -153,7 +160,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder);
|
||||
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);
|
||||
Branches branches = mock(Branches.class);
|
||||
List<Branch> branchList = Lists.newArrayList(new Branch("master",id));
|
||||
List<Branch> branchList = Lists.newArrayList(createBranch("master"));
|
||||
when(branches.getBranches()).thenReturn(branchList);
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(branches);
|
||||
MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/");
|
||||
@@ -161,9 +168,85 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
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("\"id\":\"%s\"", REVISION)));
|
||||
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 shouldCreateNewBranch() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches());
|
||||
when(branchCommandBuilder.branch("new_branch")).thenReturn(createBranch("new_branch"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/")
|
||||
.content("{\"name\": \"new_branch\"}".getBytes())
|
||||
.contentType(VndMediaType.BRANCH_REQUEST);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(
|
||||
URI.create("/v2/repositories/space/repo/branches/new_branch"),
|
||||
response.getOutputHeaders().getFirst("Location"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateNewBranchWithParent() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch")));
|
||||
when(branchCommandBuilder.from("existing_branch")).thenReturn(branchCommandBuilder);
|
||||
when(branchCommandBuilder.branch("new_branch")).thenReturn(createBranch("new_branch"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/")
|
||||
.content("{\"name\": \"new_branch\",\"parent\": \"existing_branch\"}".getBytes())
|
||||
.contentType(VndMediaType.BRANCH_REQUEST);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(
|
||||
URI.create("/v2/repositories/space/repo/branches/new_branch"),
|
||||
response.getOutputHeaders().getFirst("Location"));
|
||||
verify(branchCommandBuilder).from("existing_branch");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateExistingBranchAgain() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch")));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/")
|
||||
.content("{\"name\": \"existing_branch\"}".getBytes())
|
||||
.contentType(VndMediaType.BRANCH_REQUEST);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(409, response.getStatus());
|
||||
verify(branchCommandBuilder, never()).branch(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailForMissingParentBranch() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch")));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/")
|
||||
.content("{\"name\": \"new_branch\",\"parent\": \"no_such_branch\"}".getBytes())
|
||||
.contentType(VndMediaType.BRANCH_REQUEST);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(404, response.getStatus());
|
||||
verify(branchCommandBuilder, never()).branch(anyString());
|
||||
}
|
||||
|
||||
private Branch createBranch(String existing_branch) {
|
||||
return Branch.normalBranch(existing_branch, REVISION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class BranchToBranchDtoMapperTest {
|
||||
});
|
||||
mapper.setRegistry(registry);
|
||||
|
||||
Branch branch = new Branch("master", "42");
|
||||
Branch branch = Branch.normalBranch("master", "42");
|
||||
|
||||
BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold"));
|
||||
assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master");
|
||||
|
||||
Reference in New Issue
Block a user