diff --git a/scm-core/src/main/java/sonia/scm/DisplayManager.java b/scm-core/src/main/java/sonia/scm/DisplayManager.java new file mode 100644 index 0000000000..72113a483d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/DisplayManager.java @@ -0,0 +1,19 @@ +package sonia.scm; + +import java.util.Collection; +import java.util.Optional; + +public interface DisplayManager { + + int DEFAULT_LIMIT = 5; + + /** + * Returns a {@link Collection} of filtered objects + * + * @param filter the searched string + * @return filtered object from the store + */ + Collection autocomplete(String filter); + + Optional get(String id); +} diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 390777958d..9a7f21d3ef 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -47,8 +47,6 @@ public interface Manager extends HandlerBase, LastModifiedAware { - int DEFAULT_LIMIT = 5; - /** * Reloads a object from store and overwrites all changes. diff --git a/scm-core/src/main/java/sonia/scm/TransformFilter.java b/scm-core/src/main/java/sonia/scm/TransformFilter.java index e872ab384f..fa798fe36e 100644 --- a/scm-core/src/main/java/sonia/scm/TransformFilter.java +++ b/scm-core/src/main/java/sonia/scm/TransformFilter.java @@ -39,8 +39,10 @@ package sonia.scm; * @author Sebastian Sdorra * * @param type of objects to transform + * @param result type of the transformation */ -public interface TransformFilter +@FunctionalInterface +public interface TransformFilter { /** @@ -52,5 +54,5 @@ public interface TransformFilter * * @return tranformed object */ - public T accept(T item); + R accept(T item); } diff --git a/scm-core/src/main/java/sonia/scm/group/DisplayGroup.java b/scm-core/src/main/java/sonia/scm/group/DisplayGroup.java new file mode 100644 index 0000000000..a16a3046e9 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/DisplayGroup.java @@ -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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupDisplayManager.java b/scm-core/src/main/java/sonia/scm/group/GroupDisplayManager.java new file mode 100644 index 0000000000..d9188e9402 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/group/GroupDisplayManager.java @@ -0,0 +1,6 @@ +package sonia.scm.group; + +import sonia.scm.DisplayManager; + +public interface GroupDisplayManager extends DisplayManager { +} diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManager.java b/scm-core/src/main/java/sonia/scm/group/GroupManager.java index 08057ae3db..288196894d 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManager.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManager.java @@ -61,14 +61,4 @@ public interface GroupManager * @return all groups assigned to the given member */ public Collection getGroupsForMember(String member); - - - /** - * Returns a {@link java.util.Collection} of filtered objects - * - * @param filter the searched string - * @return filtered object from the store - */ - Collection autocomplete(String filter); - } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java index ef6de4164c..e2367d863c 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java @@ -109,11 +109,6 @@ public class GroupManagerDecorator return decorated.getGroupsForMember(member); } - @Override - public Collection autocomplete(String filter) { - return decorated.autocomplete(filter); - } - //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-core/src/main/java/sonia/scm/repository/Branch.java b/scm-core/src/main/java/sonia/scm/repository/Branch.java index ce1d43c82b..c0a7289912 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branch.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branch.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java new file mode 100644 index 0000000000..4ec78c9bdc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchCommandBuilder.java @@ -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(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchRequest.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchRequest.java new file mode 100644 index 0000000000..8473567156 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchRequest.java @@ -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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index fa09cea2cb..e380727769 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -53,6 +53,11 @@ public enum Command */ BRANCHES, + /** + * @since 2.0 + */ + BRANCH, + /** * @since 1.31 */ diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java index fe43fee038..7e016db430 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java @@ -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 getChangesets() { - Iterable changesets = - provider.handleRequest(request).getChangesets(); + HookChangesetResponse hookChangesetResponse = provider.handleRequest(request); + Iterable changesets = hookChangesetResponse.getChangesets(); if (!disablePreProcessors) { diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index ad53c3a8f7..e11afa4be9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -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()); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java new file mode 100644 index 0000000000..c659c7fac0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchCommand.java @@ -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); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java b/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java new file mode 100644 index 0000000000..c7c378500b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/IntegrateChangesFromWorkdirException.java @@ -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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index 77201d1a72..a82eb7c30a 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -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 * diff --git a/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java b/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java new file mode 100644 index 0000000000..a33af3ecb1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/CloseableWrapper.java @@ -0,0 +1,21 @@ +package sonia.scm.repository.util; + +import java.util.function.Consumer; + +public class CloseableWrapper implements AutoCloseable { + + private final T wrapped; + private final Consumer cleanup; + + public CloseableWrapper(T wrapped, Consumer cleanup) { + this.wrapped = wrapped; + this.cleanup = cleanup; + } + + public T get() { return wrapped; } + + @Override + public void close() { + cleanup.accept(wrapped); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java new file mode 100644 index 0000000000..0872242612 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/SimpleWorkdirFactory.java @@ -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 implements WorkdirFactory { + + 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 createWorkingCopy(C context) { + try { + File directory = createNewWorkdir(); + ParentAndClone 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 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 { + 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; + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java new file mode 100644 index 0000000000..1b67c7f1eb --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkdirFactory.java @@ -0,0 +1,5 @@ +package sonia.scm.repository.util; + +public interface WorkdirFactory { + WorkingCopy createWorkingCopy(C context); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java b/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java new file mode 100644 index 0000000000..3c96184142 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/util/WorkingCopy.java @@ -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 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 cleanup; + + public WorkingCopy(R workingRepository, R centralRepository, Consumer 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); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java index 90c65e3456..281e00a517 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java @@ -159,17 +159,17 @@ public final class SearchUtil * * @return */ - public static Collection search(SearchRequest searchRequest, - Collection collection, TransformFilter filter) + public static Collection search(SearchRequest searchRequest, + Collection collection, TransformFilter filter) { - List items = new ArrayList(); + List items = new ArrayList<>(); int index = 0; int counter = 0; Iterator it = collection.iterator(); while (it.hasNext()) { - T item = filter.accept(it.next()); + R item = filter.accept(it.next()); if (item != null) { diff --git a/scm-core/src/main/java/sonia/scm/user/DisplayUser.java b/scm-core/src/main/java/sonia/scm/user/DisplayUser.java new file mode 100644 index 0000000000..7a11dfbab4 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/DisplayUser.java @@ -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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/user/UserDisplayManager.java b/scm-core/src/main/java/sonia/scm/user/UserDisplayManager.java new file mode 100644 index 0000000000..159025f5ec --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/UserDisplayManager.java @@ -0,0 +1,6 @@ +package sonia.scm.user; + +import sonia.scm.DisplayManager; + +public interface UserDisplayManager extends DisplayManager { +} diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java index f301c1f2b1..a735918c59 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java @@ -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 autocomplete(String filter); - /** * Changes the password of the logged in user. * @param oldPassword The current encrypted password of the user. diff --git a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java index 0384fe1b52..0b88d856ff 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java @@ -121,11 +121,6 @@ public class UserManagerDecorator extends ManagerDecorator return decorated.getDefaultType(); } - @Override - public Collection autocomplete(String filter) { - return decorated.autocomplete(filter); - } - @Override public void changePasswordForLoggedInUser(String oldPassword, String newPassword) { decorated.changePasswordForLoggedInUser(oldPassword, newPassword); diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index d0a64b17ec..4dcb2f1d96 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -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; diff --git a/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java new file mode 100644 index 0000000000..2d3c2ed59e --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/util/SimpleWorkdirFactoryTest.java @@ -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 simpleWorkdirFactory; + + @Before + public void initFactory() throws IOException { + simpleWorkdirFactory = new SimpleWorkdirFactory(temporaryFolder.newFolder()) { + @Override + protected Repository getScmRepository(Context context) { + return REPOSITORY; + } + + @Override + protected void closeRepository(Closeable repository) throws IOException { + repository.close(); + } + + @Override + protected ParentAndClone cloneRepository(Context context, File target) { + return new ParentAndClone<>(parent, clone); + } + }; + } + + @Test + public void shouldCreateParentAndClone() { + Context context = new Context(); + try (WorkingCopy 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 workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {} + + verify(parent).close(); + } + + @Test + public void shouldCloseClone() throws IOException { + Context context = new Context(); + try (WorkingCopy workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {} + + verify(clone).close(); + } + + private static class Context {} +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java deleted file mode 100644 index 553f0f5a00..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/CloseableWrapper.java +++ /dev/null @@ -1,25 +0,0 @@ -package sonia.scm.repository; - -import java.util.function.Consumer; - -public class CloseableWrapper implements AutoCloseable { - - private final C wrapped; - private final Consumer cleanup; - - public CloseableWrapper(C wrapped, Consumer 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); - } - } -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java index f93713a221..d3ed353677 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitWorkdirFactory.java @@ -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 { } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java new file mode 100644 index 0000000000..a0ea403feb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java @@ -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 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 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())); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index 4922752b6f..ed0ac772f1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -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 getBranches() throws IOException { - List branches = null; + Git git = createGit(); - Git git = new Git(open()); + String defaultBranchName = determineDefaultBranchName(git); - try - { - List refs = git.branchList().call(); - - branches = Lists.transform(refs, new Function() - { - - @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 getRepositoryHeadRef(Git git) { + return GitUtil.getRepositoryHeadRef(git.getRepository()); } } + diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java index f328011815..58c71b8dac 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeCommand.java @@ -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 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); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index ef3d96d5cb..f4b19d1e85 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -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 * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java index f12818aa80..c30ff59afb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/SimpleGitWorkdirFactory.java @@ -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 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 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(); + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java deleted file mode 100644 index fd0cba510b..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/WorkingCopy.java +++ /dev/null @@ -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 { - WorkingCopy(Repository wrapped, Consumer cleanup) { - super(wrapped, cleanup); - } -} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java index e92ee7abb5..7c22559f44 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/CloseableWrapperTest.java @@ -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 wrapped = new Consumer() { + Consumer wrapped = new Consumer() { // 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 closer = spy(wrapped); + Consumer closer = spy(wrapped); - try (CloseableWrapper wrapper = new CloseableWrapper<>("test", closer)) { + AutoCloseable autoCloseable = () -> {}; + try (CloseableWrapper wrapper = new CloseableWrapper<>(autoCloseable, closer)) { // nothing to do here } - verify(closer).accept("test"); + verify(closer).accept(autoCloseable); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java index 5a721a7aa5..19197d009d 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitBranchCommand.java @@ -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) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index f2a4ed4954..8c4b682b18 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -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; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java new file mode 100644 index 0000000000..49800fc9e8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java @@ -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); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java new file mode 100644 index 0000000000..364306c033 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java @@ -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 readBranches(GitContext context) throws IOException { + return new GitBranchesCommand(context, repository).getBranches(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java new file mode 100644 index 0000000000..1c737deab8 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java @@ -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 getRepositoryHeadRef(Git git) { + return of(master); + } + }; + when(git.branchList()).thenReturn(listBranchCommand); + } + + @Test + void shouldCreateEmptyListWithoutBranches() throws IOException, GitAPIException { + when(listBranchCommand.call()).thenReturn(emptyList()); + + List branches = branchesCommand.getBranches(); + + assertThat(branches).isEmpty(); + } + + @Test + void shouldMapNormalBranch() throws IOException, GitAPIException { + Ref branch = createRef("branch", "1337"); + when(listBranchCommand.call()).thenReturn(asList(branch)); + + List 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 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 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; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index c926935496..e8a62f5b86 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -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() { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java index 70c34fa122..616df5e68e 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java @@ -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 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 workingCopy = factory.createWorkingCopy(createContext())) { + firstDirectory = workingCopy.getDirectory(); } - try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { - File secondDirectory = workingCopy.get().getDirectory(); + try (WorkingCopy 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 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; - } - } } diff --git a/scm-plugins/scm-git-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-plugins/scm-git-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index b07ce5bb3c..025f79add3 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -28,7 +28,6 @@ - diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java index d80c0c5afb..51ae6d5786 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgEnvironment.java @@ -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 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 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); - } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java index 057bd173d9..6815bdad96 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java @@ -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 httpServletRequestProvider, - AdvancedHttpClient httpClient) + Provider 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 httpServletRequestProvider; + + private final AccessTokenBuilderFactory accessTokenBuilderFactory; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index d2da936c48..4ccf13e738 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -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 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java new file mode 100644 index 0000000000..750bfc1332 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java @@ -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 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 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); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java index 5c38205393..55b937be4b 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java @@ -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); + } } }); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java index fee73513cd..c744abc186 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContext.java @@ -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> 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index 2c18aea2c3..d60e888cac 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -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); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java new file mode 100644 index 0000000000..515ee62c98 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgWorkdirFactory.java @@ -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 { + void configure(PullCommand pullCommand); +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java new file mode 100644 index 0000000000..9565f672d5 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/SimpleHgWorkdirFactory.java @@ -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 implements HgWorkdirFactory { + + private final Provider hgRepositoryEnvironmentBuilder; + + @Inject + public SimpleHgWorkdirFactory(Provider hgRepositoryEnvironmentBuilder) { + this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder; + } + @Override + public ParentAndClone cloneRepository(HgCommandContext context, File target) throws IOException { + BiConsumer> 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"); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index 45eba94442..2961f4ecb1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -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 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; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java index 1b31eb11ca..a2ab84c768 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgHookCallbackServlet.java @@ -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 { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java new file mode 100644 index 0000000000..2cb40cbc1b --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgRepositoryEnvironmentBuilder.java @@ -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 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 + ); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java index ba9ae3a0b9..7b66688036 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java @@ -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); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java index b6b085f3ac..427b8179ad 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgUtil.java @@ -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> 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); diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py index 6ad8081512..637aa16331 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/python/scmhooks.py @@ -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 diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java index ed222f5119..c3b66525f9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java @@ -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"); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java index 131dad0837..cca89d8eb5 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgTestUtil.java @@ -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; } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java index 74d1b1f742..fa7371f840 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgBranchCommand.java @@ -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()); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java index 2e06d4c6ea..d752047ba5 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/AbstractHgCommandTestBase.java @@ -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); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java new file mode 100644 index 0000000000..c015aa0f62 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchCommandTest.java @@ -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 readBranches() { + return new HgBranchesCommand(cmdContext, repository).getBranches(); + } +} diff --git a/scm-ui-components/packages/ui-components/src/Autocomplete.js b/scm-ui-components/packages/ui-components/src/Autocomplete.js index f3023e268b..adf86e37b7 100644 --- a/scm-ui-components/packages/ui-components/src/Autocomplete.js +++ b/scm-ui-components/packages/ui-components/src/Autocomplete.js @@ -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 { }; render() { - const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions } = this.props; + const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props; return (
- loadingMessage} - noOptionsMessage={() => noOptionsMessage} - isValidNewOption={this.isValidNewOption} - onCreateOption={value => { - this.handleInputChange({ - label: value, - value: { id: value, displayName: value } - }); - }} - /> + {creatable? + loadingMessage} + noOptionsMessage={() => noOptionsMessage} + isValidNewOption={this.isValidNewOption} + onCreateOption={value => { + this.handleInputChange({ + label: value, + value: { id: value, displayName: value } + }); + }} + /> + : + loadingMessage} + noOptionsMessage={() => noOptionsMessage} + /> + + }
); diff --git a/scm-ui/src/config/components/form/BaseUrlSettings.js b/scm-ui/src/config/components/form/BaseUrlSettings.js index 3fa0e67b84..c5414d6a60 100644 --- a/scm-ui/src/config/components/form/BaseUrlSettings.js +++ b/scm-ui/src/config/components/form/BaseUrlSettings.js @@ -19,15 +19,6 @@ class BaseUrlSettings extends React.Component {
-
- -
{ helpText={t("help.baseUrlHelpText")} />
+
+ +
); diff --git a/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java b/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java new file mode 100644 index 0000000000..5ad4f6343a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java @@ -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 implements DisplayManager { + + private final GenericDAO dao; + private final Function transform; + + protected GenericDisplayManager(GenericDAO dao, Function transform) { + this.dao = dao; + this.transform = transform; + } + + @Override + public Collection 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 get(String id) { + return ofNullable(dao.get(id)).map(transform); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 7c7dec47ff..a3661a25f0 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -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 diff --git a/scm-webapp/src/main/java/sonia/scm/api/ContextualFallbackExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/ContextualFallbackExceptionMapper.java new file mode 100644 index 0000000000..6034ff3df5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/ContextualFallbackExceptionMapper.java @@ -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 { + + 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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java index 005e29e9ea..38b421f573 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -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 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 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 List map(Collection autocomplete) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java index 51ce9e6f5a..b394c4215f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionToDtoMapper.java @@ -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 branches) { - return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches))); + public HalRepresentation map(Repository repository, Collection branches) { + return new HalRepresentation( + createLinks(repository), + embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches))); } public List getBranchDtoList(String namespace, String name, Collection 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 dtos) { return embeddedBuilder() .with("branches", dtos) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java index 343d9c8bc8..c66428697d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java @@ -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); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java new file mode 100644 index 0000000000..3db338ea85 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java @@ -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; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index d19c1a9e03..f0edbf79fb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -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(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index 479a43aef1..1ae67c4282 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -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))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1fc6b2a442..ff1013bb76 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -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() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 6e90b4e6ec..e2a6cc797e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -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; diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupDisplayManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupDisplayManager.java new file mode 100644 index 0000000000..abffa46e41 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupDisplayManager.java @@ -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 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()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index 6bf850f99d..3320773e57 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -195,7 +195,7 @@ public class DefaultGroupManager extends AbstractGroupManager final PermissionActionCheck check = GroupPermissions.read(); return SearchUtil.search(searchRequest, groupDAO.getAll(), - new TransformFilter() + new TransformFilter() { @Override public Group accept(Group group) @@ -241,13 +241,6 @@ public class DefaultGroupManager extends AbstractGroupManager return group; } - @Override - public Collection 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 * diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserDisplayManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserDisplayManager.java new file mode 100644 index 0000000000..d27b360922 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserDisplayManager.java @@ -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 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()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 3b78267631..a6c40b24f4 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -212,13 +212,6 @@ public class DefaultUserManager extends AbstractUserManager fresh.copyProperties(user); } - @Override - public Collection 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 check = UserPermissions.read(); - return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter() { + return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter() { @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; diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index a4f4589245..41bb53de1e 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -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": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 050312b693..0ad62ddf5c 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -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": { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java index d392aefe4e..ac89d02bfb 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java @@ -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); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java new file mode 100644 index 0000000000..b9072e3167 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java @@ -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", + "valkill", + "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)); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index 9216922e19..9fb20c0922 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -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 changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + List 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 branchList = Lists.newArrayList(new Branch("master",id)); + List 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); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java index 3e64ab95b6..9c15271aa2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java @@ -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");