Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-04-02 10:35:45 +02:00
88 changed files with 1839 additions and 595 deletions

View File

@@ -0,0 +1,19 @@
package sonia.scm;
import java.util.Collection;
import java.util.Optional;
public interface DisplayManager<T extends ReducedModelObject> {
int DEFAULT_LIMIT = 5;
/**
* Returns a {@link Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<T> autocomplete(String filter);
Optional<T> get(String id);
}

View File

@@ -47,8 +47,6 @@ public interface Manager<T extends ModelObject>
extends HandlerBase<T>, LastModifiedAware extends HandlerBase<T>, LastModifiedAware
{ {
int DEFAULT_LIMIT = 5;
/** /**
* Reloads a object from store and overwrites all changes. * Reloads a object from store and overwrites all changes.

View File

@@ -39,8 +39,10 @@ package sonia.scm;
* @author Sebastian Sdorra * @author Sebastian Sdorra
* *
* @param <T> type of objects to transform * @param <T> type of objects to transform
* @param <R> result type of the transformation
*/ */
public interface TransformFilter<T> @FunctionalInterface
public interface TransformFilter<T, R>
{ {
/** /**
@@ -52,5 +54,5 @@ public interface TransformFilter<T>
* *
* @return tranformed object * @return tranformed object
*/ */
public T accept(T item); R accept(T item);
} }

View File

@@ -0,0 +1,26 @@
package sonia.scm.group;
import sonia.scm.ReducedModelObject;
public class DisplayGroup implements ReducedModelObject {
private final String id;
private final String displayName;
public static DisplayGroup from(Group group) {
return new DisplayGroup(group.getId(), group.getDescription());
}
private DisplayGroup(String id, String displayName) {
this.id = id;
this.displayName = displayName;
}
public String getId() {
return id;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -0,0 +1,6 @@
package sonia.scm.group;
import sonia.scm.DisplayManager;
public interface GroupDisplayManager extends DisplayManager<DisplayGroup> {
}

View File

@@ -61,14 +61,4 @@ public interface GroupManager
* @return all groups assigned to the given member * @return all groups assigned to the given member
*/ */
public Collection<Group> getGroupsForMember(String member); public Collection<Group> getGroupsForMember(String member);
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<Group> autocomplete(String filter);
} }

View File

@@ -109,11 +109,6 @@ public class GroupManagerDecorator
return decorated.getGroupsForMember(member); return decorated.getGroupsForMember(member);
} }
@Override
public Collection<Group> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */

View File

@@ -66,7 +66,7 @@ public final class Branch implements Serializable
* This constructor should only be called from JAXB. * This constructor should only be called from JAXB.
* *
*/ */
public Branch() {} Branch() {}
/** /**
* Constructs a new branch. * Constructs a new branch.
@@ -75,10 +75,19 @@ public final class Branch implements Serializable
* @param name name of the branch * @param name name of the branch
* @param revision latest revision 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.name = name;
this.revision = revision; 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 -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -107,7 +116,8 @@ public final class Branch implements Serializable
final Branch other = (Branch) obj; final Branch other = (Branch) obj;
return Objects.equal(name, other.name) 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; return revision;
} }
public boolean isDefaultBranch() {
return defaultBranch;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** name of the branch */ /** name of the branch */
@@ -169,4 +183,6 @@ public final class Branch implements Serializable
/** Field description */ /** Field description */
private String revision; private String revision;
private boolean defaultBranch;
} }

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -53,6 +53,11 @@ public enum Command
*/ */
BRANCHES, BRANCHES,
/**
* @since 2.0
*/
BRANCH,
/** /**
* @since 1.31 * @since 1.31
*/ */

View File

@@ -46,6 +46,7 @@ import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.HookChangesetProvider; import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest; import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -115,8 +116,8 @@ public final class HookChangesetBuilder
*/ */
public Iterable<Changeset> getChangesets() public Iterable<Changeset> getChangesets()
{ {
Iterable<Changeset> changesets = HookChangesetResponse hookChangesetResponse = provider.handleRequest(request);
provider.handleRequest(request).getChangesets(); Iterable<Changeset> changesets = hookChangesetResponse.getChangesets();
if (!disablePreProcessors) if (!disablePreProcessors)
{ {

View File

@@ -39,6 +39,7 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceProvider;
import java.io.Closeable; import java.io.Closeable;
@@ -82,10 +83,9 @@ import java.util.stream.Stream;
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder * @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17 * @since 1.17
*/ */
@Slf4j
public final class RepositoryService implements Closeable { 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 CacheManager cacheManager;
private final PreProcessorUtil preProcessorUtil; private final PreProcessorUtil preProcessorUtil;
@@ -131,7 +131,7 @@ public final class RepositoryService implements Closeable {
try { try {
provider.close(); provider.close();
} catch (IOException ex) { } 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. * by the implementation of the repository service provider.
*/ */
public BlameCommandBuilder getBlameCommand() { public BlameCommandBuilder getBlameCommand() {
logger.debug("create blame command for repository {}", LOG.debug("create blame command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(), return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
@@ -158,13 +158,28 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider. * by the implementation of the repository service provider.
*/ */
public BranchesCommandBuilder getBranchesCommand() { public BranchesCommandBuilder getBranchesCommand() {
logger.debug("create branches command for repository {}", LOG.debug("create branches command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new BranchesCommandBuilder(cacheManager, return new BranchesCommandBuilder(cacheManager,
provider.getBranchesCommand(), repository); 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. * 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. * by the implementation of the repository service provider.
*/ */
public BrowseCommandBuilder getBrowseCommand() { public BrowseCommandBuilder getBrowseCommand() {
logger.debug("create browse command for repository {}", LOG.debug("create browse command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(), return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
@@ -189,7 +204,7 @@ public final class RepositoryService implements Closeable {
* @since 1.43 * @since 1.43
*/ */
public BundleCommandBuilder getBundleCommand() { public BundleCommandBuilder getBundleCommand() {
logger.debug("create bundle command for repository {}", LOG.debug("create bundle command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new BundleCommandBuilder(provider.getBundleCommand(), repository); return new BundleCommandBuilder(provider.getBundleCommand(), repository);
@@ -203,7 +218,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider. * by the implementation of the repository service provider.
*/ */
public CatCommandBuilder getCatCommand() { public CatCommandBuilder getCatCommand() {
logger.debug("create cat command for repository {}", LOG.debug("create cat command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new CatCommandBuilder(provider.getCatCommand()); return new CatCommandBuilder(provider.getCatCommand());
@@ -218,7 +233,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider. * by the implementation of the repository service provider.
*/ */
public DiffCommandBuilder getDiffCommand() { public DiffCommandBuilder getDiffCommand() {
logger.debug("create diff command for repository {}", LOG.debug("create diff command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures()); return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
@@ -234,7 +249,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31 * @since 1.31
*/ */
public IncomingCommandBuilder getIncomingCommand() { public IncomingCommandBuilder getIncomingCommand() {
logger.debug("create incoming command for repository {}", LOG.debug("create incoming command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new IncomingCommandBuilder(cacheManager, return new IncomingCommandBuilder(cacheManager,
@@ -249,7 +264,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider. * by the implementation of the repository service provider.
*/ */
public LogCommandBuilder getLogCommand() { public LogCommandBuilder getLogCommand() {
logger.debug("create log command for repository {}", LOG.debug("create log command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new LogCommandBuilder(cacheManager, provider.getLogCommand(), return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
@@ -264,7 +279,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider. * by the implementation of the repository service provider.
*/ */
public ModificationsCommandBuilder getModificationsCommand() { 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); 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 * @since 1.31
*/ */
public OutgoingCommandBuilder getOutgoingCommand() { public OutgoingCommandBuilder getOutgoingCommand() {
logger.debug("create outgoing command for repository {}", LOG.debug("create outgoing command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new OutgoingCommandBuilder(cacheManager, return new OutgoingCommandBuilder(cacheManager,
@@ -293,7 +308,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31 * @since 1.31
*/ */
public PullCommandBuilder getPullCommand() { public PullCommandBuilder getPullCommand() {
logger.debug("create pull command for repository {}", LOG.debug("create pull command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new PullCommandBuilder(provider.getPullCommand(), repository); return new PullCommandBuilder(provider.getPullCommand(), repository);
@@ -308,7 +323,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31 * @since 1.31
*/ */
public PushCommandBuilder getPushCommand() { public PushCommandBuilder getPushCommand() {
logger.debug("create push command for repository {}", LOG.debug("create push command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new PushCommandBuilder(provider.getPushCommand()); return new PushCommandBuilder(provider.getPushCommand());
@@ -331,7 +346,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider. * by the implementation of the repository service provider.
*/ */
public TagsCommandBuilder getTagsCommand() { public TagsCommandBuilder getTagsCommand() {
logger.debug("create tags command for repository {}", LOG.debug("create tags command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(), return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
@@ -347,7 +362,7 @@ public final class RepositoryService implements Closeable {
* @since 1.43 * @since 1.43
*/ */
public UnbundleCommandBuilder getUnbundleCommand() { public UnbundleCommandBuilder getUnbundleCommand() {
logger.debug("create unbundle command for repository {}", LOG.debug("create unbundle command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new UnbundleCommandBuilder(provider.getUnbundleCommand(), return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
@@ -364,7 +379,8 @@ public final class RepositoryService implements Closeable {
* @since 2.0.0 * @since 2.0.0
*/ */
public MergeCommandBuilder getMergeCommand() { public MergeCommandBuilder getMergeCommand() {
logger.debug("create merge command for repository {}", RepositoryPermissions.push(getRepository()).check();
LOG.debug("create merge command for repository {}",
repository.getNamespaceAndName()); repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand()); return new MergeCommandBuilder(provider.getMergeCommand());

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -101,6 +101,17 @@ public abstract class RepositoryServiceProvider implements Closeable
throw new CommandNotSupportedException(Command.BRANCHES); throw new CommandNotSupportedException(Command.BRANCHES);
} }
/**
* Method description
*
*
* @return
*/
public BranchCommand getBranchCommand()
{
throw new CommandNotSupportedException(Command.BRANCH);
}
/** /**
* Method description * Method description
* *

View File

@@ -0,0 +1,21 @@
package sonia.scm.repository.util;
import java.util.function.Consumer;
public class CloseableWrapper<T extends AutoCloseable> implements AutoCloseable {
private final T wrapped;
private final Consumer<T> cleanup;
public CloseableWrapper(T wrapped, Consumer<T> cleanup) {
this.wrapped = wrapped;
this.cleanup = cleanup;
}
public T get() { return wrapped; }
@Override
public void close() {
cleanup.accept(wrapped);
}
}

View File

@@ -0,0 +1,77 @@
package sonia.scm.repository.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public abstract class SimpleWorkdirFactory<R, C> implements WorkdirFactory<R, C> {
private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class);
private final File poolDirectory;
public SimpleWorkdirFactory() {
this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work"));
}
public SimpleWorkdirFactory(File poolDirectory) {
this.poolDirectory = poolDirectory;
if (!poolDirectory.exists() && !poolDirectory.mkdirs()) {
throw new IllegalStateException("could not create pool directory " + poolDirectory);
}
}
@Override
public WorkingCopy<R> createWorkingCopy(C context) {
try {
File directory = createNewWorkdir();
ParentAndClone<R> parentAndClone = cloneRepository(context, directory);
return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory);
} catch (IOException e) {
throw new InternalRepositoryException(getScmRepository(context), "could not clone repository in temporary directory", e);
}
}
protected abstract Repository getScmRepository(C context);
@SuppressWarnings("squid:S00112")
// We do allow implementations to throw arbitrary exceptions here, so that we can handle them in close
protected abstract void closeRepository(R repository) throws Exception;
protected abstract ParentAndClone<R> cloneRepository(C context, File target) throws IOException;
private File createNewWorkdir() throws IOException {
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
}
private void close(R repository) {
try {
closeRepository(repository);
} catch (Exception e) {
logger.warn("could not close temporary repository clone", e);
}
}
protected static class ParentAndClone<R> {
private final R parent;
private final R clone;
public ParentAndClone(R parent, R clone) {
this.parent = parent;
this.clone = clone;
}
public R getParent() {
return parent;
}
public R getClone() {
return clone;
}
}
}

View File

@@ -0,0 +1,5 @@
package sonia.scm.repository.util;
public interface WorkdirFactory<R, C> {
WorkingCopy<R> createWorkingCopy(C context);
}

View File

@@ -0,0 +1,49 @@
package sonia.scm.repository.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.util.IOUtil;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
public class WorkingCopy<R> implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(WorkingCopy.class);
private final File directory;
private final R workingRepository;
private final R centralRepository;
private final Consumer<R> cleanup;
public WorkingCopy(R workingRepository, R centralRepository, Consumer<R> cleanup, File directory) {
this.directory = directory;
this.workingRepository = workingRepository;
this.centralRepository = centralRepository;
this.cleanup = cleanup;
}
public R getWorkingRepository() {
return workingRepository;
}
public R getCentralRepository() {
return centralRepository;
}
public File getDirectory() {
return directory;
}
@Override
public void close() {
try {
cleanup.accept(workingRepository);
cleanup.accept(centralRepository);
IOUtil.delete(directory);
} catch (IOException e) {
LOG.warn("could not delete temporary workdir '{}'", directory, e);
}
}
}

View File

@@ -159,17 +159,17 @@ public final class SearchUtil
* *
* @return * @return
*/ */
public static <T> Collection<T> search(SearchRequest searchRequest, public static <T, R> Collection<R> search(SearchRequest searchRequest,
Collection<T> collection, TransformFilter<T> filter) Collection<T> collection, TransformFilter<T, R> filter)
{ {
List<T> items = new ArrayList<T>(); List<R> items = new ArrayList<>();
int index = 0; int index = 0;
int counter = 0; int counter = 0;
Iterator<T> it = collection.iterator(); Iterator<T> it = collection.iterator();
while (it.hasNext()) while (it.hasNext())
{ {
T item = filter.accept(it.next()); R item = filter.accept(it.next());
if (item != null) if (item != null)
{ {

View File

@@ -0,0 +1,32 @@
package sonia.scm.user;
import sonia.scm.ReducedModelObject;
public class DisplayUser implements ReducedModelObject {
private final String id;
private final String displayName;
private final String mail;
public static DisplayUser from(User user) {
return new DisplayUser(user.getId(), user.getDisplayName(), user.getMail());
}
private DisplayUser(String id, String displayName, String mail) {
this.id = id;
this.displayName = displayName;
this.mail = mail;
}
public String getId() {
return id;
}
public String getDisplayName() {
return displayName;
}
public String getMail() {
return mail;
}
}

View File

@@ -0,0 +1,6 @@
package sonia.scm.user;
import sonia.scm.DisplayManager;
public interface UserDisplayManager extends DisplayManager<DisplayUser> {
}

View File

@@ -75,14 +75,6 @@ public interface UserManager
return getDefaultType().equals(user.getType()); return getDefaultType().equals(user.getType());
} }
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<User> autocomplete(String filter);
/** /**
* Changes the password of the logged in user. * Changes the password of the logged in user.
* @param oldPassword The current encrypted password of the user. * @param oldPassword The current encrypted password of the user.

View File

@@ -121,11 +121,6 @@ public class UserManagerDecorator extends ManagerDecorator<User>
return decorated.getDefaultType(); return decorated.getDefaultType();
} }
@Override
public Collection<User> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
@Override @Override
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) { public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
decorated.changePasswordForLoggedInUser(oldPassword, newPassword); decorated.changePasswordForLoggedInUser(oldPassword, newPassword);

View File

@@ -27,6 +27,7 @@ public class VndMediaType {
public static final String TAG = PREFIX + "tag" + SUFFIX; public static final String TAG = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX;
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 DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX;
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;

View File

@@ -0,0 +1,75 @@
package sonia.scm.repository.util;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.Repository;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class SimpleWorkdirFactoryTest {
private static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
private final Closeable parent = mock(Closeable.class);
private final Closeable clone = mock(Closeable.class);
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private SimpleWorkdirFactory<Closeable, Context> simpleWorkdirFactory;
@Before
public void initFactory() throws IOException {
simpleWorkdirFactory = new SimpleWorkdirFactory<Closeable, Context>(temporaryFolder.newFolder()) {
@Override
protected Repository getScmRepository(Context context) {
return REPOSITORY;
}
@Override
protected void closeRepository(Closeable repository) throws IOException {
repository.close();
}
@Override
protected ParentAndClone<Closeable> cloneRepository(Context context, File target) {
return new ParentAndClone<>(parent, clone);
}
};
}
@Test
public void shouldCreateParentAndClone() {
Context context = new Context();
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {
assertThat(workingCopy.getCentralRepository()).isSameAs(parent);
assertThat(workingCopy.getWorkingRepository()).isSameAs(clone);
}
}
@Test
public void shouldCloseParent() throws IOException {
Context context = new Context();
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {}
verify(parent).close();
}
@Test
public void shouldCloseClone() throws IOException {
Context context = new Context();
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {}
verify(clone).close();
}
private static class Context {}
}

View File

@@ -1,25 +0,0 @@
package sonia.scm.repository;
import java.util.function.Consumer;
public class CloseableWrapper<C> implements AutoCloseable {
private final C wrapped;
private final Consumer<C> cleanup;
public CloseableWrapper(C wrapped, Consumer<C> cleanup) {
this.wrapped = wrapped;
this.cleanup = cleanup;
}
public C get() { return wrapped; }
@Override
public void close() {
try {
cleanup.accept(wrapped);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}

View File

@@ -1,8 +1,8 @@
package sonia.scm.repository; package sonia.scm.repository;
import org.eclipse.jgit.lib.Repository;
import sonia.scm.repository.spi.GitContext; import sonia.scm.repository.spi.GitContext;
import sonia.scm.repository.spi.WorkingCopy; import sonia.scm.repository.util.WorkdirFactory;
public interface GitWorkdirFactory { public interface GitWorkdirFactory extends WorkdirFactory<Repository, GitContext> {
WorkingCopy createWorkingCopy(GitContext gitContext);
} }

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import sonia.scm.repository.Branch;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.util.WorkingCopy;
import java.util.stream.StreamSupport;
public class GitBranchCommand extends AbstractGitCommand implements BranchCommand {
private final GitWorkdirFactory workdirFactory;
GitBranchCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) {
super(context, repository);
this.workdirFactory = workdirFactory;
}
@Override
public Branch branch(BranchRequest request) {
try (WorkingCopy<org.eclipse.jgit.lib.Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
Git clone = new Git(workingCopy.getWorkingRepository());
if (request.getParentBranch() != null) {
clone.checkout().setName(request.getParentBranch());
}
Ref ref = clone.branchCreate().setName(request.getNewBranch()).call();
Iterable<PushResult> call = clone.push().add(request.getNewBranch()).call();
StreamSupport.stream(call.spliterator(), false)
.flatMap(pushResult -> pushResult.getRemoteUpdates().stream())
.filter(remoteRefUpdate -> remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK)
.findFirst()
.ifPresent(r -> this.handlePushError(r, request, context.getRepository()));
return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId()));
} catch (GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex);
}
}
private void handlePushError(RemoteRefUpdate remoteRefUpdate, BranchRequest request, Repository repository) {
if (remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) {
// TODO handle failed remote update
throw new IntegrateChangesFromWorkdirException(repository,
String.format("Could not push new branch '%s' into central repository", request.getNewBranch()));
}
}
}

View File

@@ -34,11 +34,14 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Function; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists; import com.google.common.base.Strings;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
@@ -46,6 +49,8 @@ import sonia.scm.repository.Repository;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -53,17 +58,10 @@ import java.util.List;
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitBranchesCommand extends AbstractGitCommand public class GitBranchesCommand extends AbstractGitCommand implements BranchesCommand {
implements BranchesCommand
{ private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class);
/**
* Constructs ...
*
*
* @param context
* @param repository
*/
public GitBranchesCommand(GitContext context, Repository repository) public GitBranchesCommand(GitContext context, Repository repository)
{ {
super(context, repository); super(context, repository);
@@ -73,38 +71,54 @@ public class GitBranchesCommand extends AbstractGitCommand
@Override @Override
public List<Branch> getBranches() throws IOException { public List<Branch> getBranches() throws IOException {
List<Branch> branches = null; Git git = createGit();
Git git = new Git(open()); String defaultBranchName = determineDefaultBranchName(git);
try try {
{ return git
List<Ref> refs = git.branchList().call(); .branchList()
.call()
branches = Lists.transform(refs, new Function<Ref, Branch>() .stream()
{ .map(ref -> createBranchObject(defaultBranchName, ref))
.collect(Collectors.toList());
@Override } catch (GitAPIException ex) {
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)
{
throw new InternalRepositoryException(repository, "could not read branches", ex); throw new InternalRepositoryException(repository, "could not read branches", ex);
} }
}
return branches; @VisibleForTesting
Git createGit() throws IOException {
return new Git(open());
}
@Nullable
private Branch createBranchObject(String defaultBranchName, Ref ref) {
String branchName = GitUtil.getBranch(ref);
if (branchName == null) {
LOG.warn("could not determine branch name for branch name {} at revision {}", ref.getName(), ref.getObjectId());
return null;
} else {
if (branchName.equals(defaultBranchName)) {
return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId()));
} else {
return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId()));
}
}
}
private String determineDefaultBranchName(Git git) {
String defaultBranchName = context.getConfig().getDefaultBranch();
if (Strings.isNullOrEmpty(defaultBranchName)) {
return getRepositoryHeadRef(git).map(GitUtil::getBranch).orElse(null);
} else {
return defaultBranchName;
}
}
Optional<Ref> getRepositoryHeadRef(Git git) {
return GitUtil.getRepositoryHeadRef(git.getRepository());
} }
} }

View File

@@ -20,6 +20,7 @@ import sonia.scm.repository.Person;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.MergeCommandResult; import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult; import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.util.WorkingCopy;
import sonia.scm.user.User; import sonia.scm.user.User;
import java.io.IOException; import java.io.IOException;
@@ -46,10 +47,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
@Override @Override
public MergeCommandResult merge(MergeCommandRequest request) { public MergeCommandResult merge(MergeCommandRequest request) {
RepositoryPermissions.push(context.getRepository().getId()).check(); try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
Repository repository = workingCopy.getWorkingRepository();
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
Repository repository = workingCopy.get();
logger.debug("cloned repository to folder {}", repository.getWorkTree()); logger.debug("cloned repository to folder {}", repository.getWorkTree());
return new MergeWorker(repository, request).merge(); return new MergeWorker(repository, request).merge();
} catch (IOException e) { } catch (IOException e) {
@@ -186,7 +185,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
try { try {
clone.push().call(); clone.push().call();
} catch (GitAPIException e) { } catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + target + " to origin", e); throw new IntegrateChangesFromWorkdirException(repository,
"could not push merged branch " + target + " into central repository", e);
} }
logger.debug("pushed merged branch {}", target); logger.debug("pushed merged branch {}", target);
} }

View File

@@ -120,6 +120,18 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitBranchesCommand(context, repository); return new GitBranchesCommand(context, repository);
} }
/**
* Method description
*
*
* @return
*/
@Override
public BranchCommand getBranchCommand()
{
return new GitBranchCommand(context, repository, handler.getWorkdirFactory());
}
/** /**
* Method description * Method description
* *

View File

@@ -4,64 +4,49 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.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.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.util.SimpleWorkdirFactory;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class SimpleGitWorkdirFactory implements GitWorkdirFactory { public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
private static final Logger logger = LoggerFactory.getLogger(SimpleGitWorkdirFactory.class);
private final File poolDirectory;
public SimpleGitWorkdirFactory() { public SimpleGitWorkdirFactory() {
this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
} }
public SimpleGitWorkdirFactory(File poolDirectory) { SimpleGitWorkdirFactory(File poolDirectory) {
this.poolDirectory = poolDirectory; super(poolDirectory);
poolDirectory.mkdirs();
} }
public WorkingCopy createWorkingCopy(GitContext gitContext) { @Override
public ParentAndClone<Repository> cloneRepository(GitContext context, File target) {
try { try {
Repository clone = cloneRepository(gitContext.getDirectory(), createNewWorkdir()); return new ParentAndClone<>(null, Git.cloneRepository()
return new WorkingCopy(clone, this::close); .setURI(createScmTransportProtocolUri(context.getDirectory()))
} 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);
}
}
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) .setDirectory(target)
.call() .call()
.getRepository(); .getRepository());
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e);
}
} }
private String createScmTransportProtocolUri(File bareRepository) { private String createScmTransportProtocolUri(File bareRepository) {
return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath(); return ScmTransportProtocol.NAME + "://" + bareRepository.getAbsolutePath();
} }
private void close(Repository repository) { @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(); 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 sonia.scm.repository.Repository getScmRepository(GitContext context) {
return context.getRepository();
}
} }

View File

@@ -1,12 +0,0 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.lib.Repository;
import sonia.scm.repository.CloseableWrapper;
import java.util.function.Consumer;
public class WorkingCopy extends CloseableWrapper<Repository> {
WorkingCopy(Repository wrapped, Consumer<Repository> cleanup) {
super(wrapped, cleanup);
}
}

View File

@@ -1,6 +1,7 @@
package sonia.scm.repository; package sonia.scm.repository;
import org.junit.Test; import org.junit.Test;
import sonia.scm.repository.util.CloseableWrapper;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -11,19 +12,20 @@ public class CloseableWrapperTest {
@Test @Test
public void shouldExecuteGivenMethodAtClose() { public void shouldExecuteGivenMethodAtClose() {
Consumer<String> wrapped = new Consumer<String>() { Consumer<AutoCloseable> wrapped = new Consumer<AutoCloseable>() {
// no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy // no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy
@Override @Override
public void accept(String s) { public void accept(AutoCloseable s) {
} }
}; };
Consumer<String> closer = spy(wrapped); Consumer<AutoCloseable> closer = spy(wrapped);
try (CloseableWrapper<String> wrapper = new CloseableWrapper<>("test", closer)) { AutoCloseable autoCloseable = () -> {};
try (CloseableWrapper<AutoCloseable> wrapper = new CloseableWrapper<>(autoCloseable, closer)) {
// nothing to do here // nothing to do here
} }
verify(closer).accept("test"); verify(closer).accept(autoCloseable);
} }
} }

View File

@@ -72,7 +72,7 @@ public class GitBranchCommand implements BranchCommand
try try
{ {
Ref ref = git.branchCreate().setName(name).call(); 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) catch (GitAPIException ex)
{ {

View File

@@ -34,11 +34,19 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.transport.Transport;
import org.junit.After; import org.junit.After;
import org.junit.Before;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitRepositoryConfig; import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.store.InMemoryConfigurationStoreFactory; import sonia.scm.store.InMemoryConfigurationStoreFactory;
import static com.google.inject.util.Providers.of;
import static org.mockito.Mockito.mock;
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -105,4 +113,5 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
/** Field description */ /** Field description */
private GitContext context; private GitContext context;
private ScmTransportProtocol scmTransportProtocol;
} }

View File

@@ -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);
}
}

View File

@@ -0,0 +1,35 @@
package sonia.scm.repository.spi;
import org.assertj.core.api.Assertions;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.repository.Branch;
import sonia.scm.repository.api.BranchRequest;
import java.io.IOException;
import java.util.List;
public class GitBranchCommandTest extends AbstractGitCommandTestBase {
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Test
public void shouldCreateBranch() throws IOException {
GitContext context = createContext();
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
BranchRequest branchRequest = new BranchRequest();
branchRequest.setNewBranch("new_branch");
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest);
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
}
private List<Branch> readBranches(GitContext context) throws IOException {
return new GitBranchesCommand(context, repository).getBranches();
}
}

View File

@@ -0,0 +1,117 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Branch;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.Repository;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Optional.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GitBranchesCommandTest {
@Mock
GitContext context;
@Mock
Git git;
@Mock
ListBranchCommand listBranchCommand;
@Mock
GitRepositoryConfig gitRepositoryConfig;
GitBranchesCommand branchesCommand;
private Ref master;
@BeforeEach
void initContext() {
when(context.getConfig()).thenReturn(gitRepositoryConfig);
}
@BeforeEach
void initCommand() {
master = createRef("master", "0000");
branchesCommand = new GitBranchesCommand(context, new Repository("1", "git", "space", "X")) {
@Override
Git createGit() {
return git;
}
@Override
Optional<Ref> getRepositoryHeadRef(Git git) {
return of(master);
}
};
when(git.branchList()).thenReturn(listBranchCommand);
}
@Test
void shouldCreateEmptyListWithoutBranches() throws IOException, GitAPIException {
when(listBranchCommand.call()).thenReturn(emptyList());
List<Branch> branches = branchesCommand.getBranches();
assertThat(branches).isEmpty();
}
@Test
void shouldMapNormalBranch() throws IOException, GitAPIException {
Ref branch = createRef("branch", "1337");
when(listBranchCommand.call()).thenReturn(asList(branch));
List<Branch> branches = branchesCommand.getBranches();
assertThat(branches).containsExactly(Branch.normalBranch("branch", "1337"));
}
@Test
void shouldMarkMasterBranchWithMasterFromConfig() throws IOException, GitAPIException {
Ref branch = createRef("branch", "1337");
when(listBranchCommand.call()).thenReturn(asList(branch));
when(gitRepositoryConfig.getDefaultBranch()).thenReturn("branch");
List<Branch> branches = branchesCommand.getBranches();
assertThat(branches).containsExactlyInAnyOrder(Branch.defaultBranch("branch", "1337"));
}
@Test
void shouldMarkMasterBranchWithMasterFromHead() throws IOException, GitAPIException {
Ref branch = createRef("branch", "1337");
when(listBranchCommand.call()).thenReturn(asList(branch, master));
List<Branch> branches = branchesCommand.getBranches();
assertThat(branches).containsExactlyInAnyOrder(
Branch.normalBranch("branch", "1337"),
Branch.defaultBranch("master", "0000")
);
}
private Ref createRef(String branchName, String revision) {
Ref ref = mock(Ref.class);
lenient().when(ref.getName()).thenReturn("refs/heads/" + branchName);
ObjectId objectId = mock(ObjectId.class);
lenient().when(objectId.name()).thenReturn(revision);
lenient().when(ref.getObjectId()).thenReturn(objectId);
return ref;
}
}

View File

@@ -41,27 +41,8 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@Rule
private ScmTransportProtocol scmTransportProtocol; public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Before
public void bindScmProtocol() {
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
RepositoryManager repositoryManager = mock(RepositoryManager.class);
HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory);
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
scmTransportProtocol = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler));
Transport.register(scmTransportProtocol);
when(gitRepositoryHandler.getRepositoryId(any())).thenReturn("1");
when(repositoryManager.get("1")).thenReturn(new sonia.scm.repository.Repository());
}
@After
public void unregisterScmProtocol() {
Transport.unregister(scmTransportProtocol);
}
@Test @Test
public void shouldDetectMergeableBranches() { public void shouldDetectMergeableBranches() {

View File

@@ -1,6 +1,5 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.Transport;
@@ -12,6 +11,7 @@ import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.util.WorkingCopy;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -44,39 +44,29 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File masterRepo = createRepositoryDirectory(); File masterRepo = createRepositoryDirectory();
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
assertThat(workingCopy.get().getDirectory()) assertThat(workingCopy.getDirectory())
.exists() .exists()
.isNotEqualTo(masterRepo) .isNotEqualTo(masterRepo)
.isDirectory(); .isDirectory();
assertThat(new File(workingCopy.get().getWorkTree(), "a.txt")) assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt"))
.exists() .exists()
.isFile() .isFile()
.hasContent("a\nline for blame"); .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 @Test
public void cloneFromPoolShouldNotBeReused() throws IOException { public void cloneFromPoolShouldNotBeReused() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File firstDirectory; File firstDirectory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
firstDirectory = workingCopy.get().getDirectory(); firstDirectory = workingCopy.getDirectory();
} }
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
File secondDirectory = workingCopy.get().getDirectory(); File secondDirectory = workingCopy.getDirectory();
assertThat(secondDirectory).isNotEqualTo(firstDirectory); assertThat(secondDirectory).isNotEqualTo(firstDirectory);
} }
} }
@@ -86,23 +76,9 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder()); SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File directory; File directory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) { try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
directory = workingCopy.get().getWorkTree(); directory = workingCopy.getWorkingRepository().getWorkTree();
} }
assertThat(directory).doesNotExist(); 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;
}
}
} }

View File

@@ -28,7 +28,6 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -35,15 +35,9 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings; import com.google.inject.ProvisionException;
import org.apache.shiro.codec.Base64;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.security.CipherUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.HgUtil; import sonia.scm.web.HgUtil;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -59,6 +53,8 @@ import javax.servlet.http.HttpServletRequest;
public final class HgEnvironment public final class HgEnvironment
{ {
private static final Logger LOG = LoggerFactory.getLogger(HgEnvironment.class);
/** Field description */ /** Field description */
public static final String ENV_PYTHON_PATH = "PYTHONPATH"; public static final String ENV_PYTHON_PATH = "PYTHONPATH";
@@ -68,14 +64,7 @@ public final class HgEnvironment
/** Field description */ /** Field description */
private static final String ENV_URL = "SCM_URL"; private static final String ENV_URL = "SCM_URL";
/** Field description */ private static final String SCM_BEARER_TOKEN = "SCM_BEARER_TOKEN";
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
/**
* the logger for HgEnvironment
*/
private static final Logger logger =
LoggerFactory.getLogger(HgEnvironment.class);
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@@ -87,6 +76,20 @@ public final class HgEnvironment
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager)
{
prepareEnvironment(environment, handler, hookManager, null);
}
/** /**
* Method description * Method description
* *
@@ -105,65 +108,20 @@ public final class HgEnvironment
if (request != null) if (request != null)
{ {
hookUrl = hookManager.createUrl(request); hookUrl = hookManager.createUrl(request);
environment.put(SCM_CREDENTIALS, getCredentials(request));
} }
else else
{ {
hookUrl = hookManager.createUrl(); 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_PYTHON_PATH, HgUtil.getPythonPath(handler.getConfig()));
environment.put(ENV_URL, hookUrl); environment.put(ENV_URL, hookUrl);
environment.put(ENV_CHALLENGE, hookManager.getChallenge()); environment.put(ENV_CHALLENGE, hookManager.getChallenge());
} }
/**
* Method description
*
*
* @param environment
* @param handler
* @param hookManager
*/
public static void prepareEnvironment(Map<String, String> environment,
HgRepositoryHandler handler, HgHookManager hookManager)
{
prepareEnvironment(environment, handler, hookManager, null);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
private static String getCredentials(HttpServletRequest request)
{
String credentials = null;
String header = request.getHeader(HttpUtil.HEADER_AUTHORIZATION);
if (!Strings.isNullOrEmpty(header))
{
String[] parts = header.split("\\s+");
if (parts.length > 0)
{
CipherUtil cu = CipherUtil.getInstance();
credentials = cu.encode(Base64.decodeToString(parts[1]));
}
else
{
logger.warn("invalid basic authentication header");
}
}
else
{
logger.warn("could not find authentication header on request");
}
return Strings.nullToEmpty(credentials);
}
} }

View File

@@ -47,6 +47,9 @@ import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.config.ScmConfigurationChangedEvent;
import sonia.scm.net.ahc.AdvancedHttpClient; 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.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -78,19 +81,20 @@ public class HgHookManager
/** /**
* Constructs ... * Constructs ...
* *
*
* @param configuration * @param configuration
* @param httpServletRequestProvider * @param httpServletRequestProvider
* @param httpClient * @param httpClient
* @param accessTokenBuilderFactory
*/ */
@Inject @Inject
public HgHookManager(ScmConfiguration configuration, public HgHookManager(ScmConfiguration configuration,
Provider<HttpServletRequest> httpServletRequestProvider, Provider<HttpServletRequest> httpServletRequestProvider,
AdvancedHttpClient httpClient) AdvancedHttpClient httpClient, AccessTokenBuilderFactory accessTokenBuilderFactory)
{ {
this.configuration = configuration; this.configuration = configuration;
this.httpServletRequestProvider = httpServletRequestProvider; this.httpServletRequestProvider = httpServletRequestProvider;
this.httpClient = httpClient; this.httpClient = httpClient;
this.accessTokenBuilderFactory = accessTokenBuilderFactory;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -192,6 +196,13 @@ public class HgHookManager
return this.challenge.equals(challenge); return this.challenge.equals(challenge);
} }
public String getCredentials()
{
AccessToken accessToken = accessTokenBuilderFactory.create().build();
return CipherUtil.getInstance().encode(accessToken.compact());
}
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
/** /**
@@ -391,4 +402,6 @@ public class HgHookManager
/** Field description */ /** Field description */
private Provider<HttpServletRequest> httpServletRequestProvider; private Provider<HttpServletRequest> httpServletRequestProvider;
private final AccessTokenBuilderFactory accessTokenBuilderFactory;
} }

View File

@@ -53,6 +53,7 @@ import sonia.scm.io.INISection;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import sonia.scm.repository.spi.HgRepositoryServiceProvider; import sonia.scm.repository.spi.HgRepositoryServiceProvider;
import sonia.scm.repository.spi.HgWorkdirFactory;
import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import sonia.scm.util.SystemUtil; import sonia.scm.util.SystemUtil;
@@ -113,10 +114,11 @@ public class HgRepositoryHandler
public HgRepositoryHandler(ConfigurationStoreFactory storeFactory, public HgRepositoryHandler(ConfigurationStoreFactory storeFactory,
Provider<HgContext> hgContextProvider, Provider<HgContext> hgContextProvider,
RepositoryLocationResolver repositoryLocationResolver, RepositoryLocationResolver repositoryLocationResolver,
PluginLoader pluginLoader) PluginLoader pluginLoader, HgWorkdirFactory workdirFactory)
{ {
super(storeFactory, repositoryLocationResolver, pluginLoader); super(storeFactory, repositoryLocationResolver, pluginLoader);
this.hgContextProvider = hgContextProvider; this.hgContextProvider = hgContextProvider;
this.workdirFactory = workdirFactory;
try try
{ {
@@ -408,6 +410,10 @@ public class HgRepositoryHandler
} }
} }
public HgWorkdirFactory getWorkdirFactory() {
return workdirFactory;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
@@ -415,4 +421,6 @@ public class HgRepositoryHandler
/** Field description */ /** Field description */
private JAXBContext jaxbContext; private JAXBContext jaxbContext;
private final HgWorkdirFactory workdirFactory;
} }

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository.spi;
import com.aragost.javahg.Changeset;
import com.aragost.javahg.commands.CommitCommand;
import com.aragost.javahg.commands.PullCommand;
import com.aragost.javahg.commands.UpdateCommand;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Branch;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.util.WorkingCopy;
import sonia.scm.user.User;
import java.io.IOException;
/**
* Mercurial implementation of the {@link BranchCommand}.
* Note that this creates an empty commit to "persist" the new branch.
*/
public class HgBranchCommand extends AbstractCommand implements BranchCommand {
private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class);
private final HgWorkdirFactory workdirFactory;
HgBranchCommand(HgCommandContext context, Repository repository, HgWorkdirFactory workdirFactory) {
super(context, repository);
this.workdirFactory = workdirFactory;
}
@Override
public Branch branch(BranchRequest request) {
try (WorkingCopy<com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext())) {
com.aragost.javahg.Repository repository = workingCopy.getWorkingRepository();
checkoutParentBranchIfSpecified(request, repository);
Changeset emptyChangeset = createNewBranchWithEmptyCommit(request, repository);
LOG.debug("Created new branch '{}' in repository {} with changeset {}",
request.getNewBranch(), getRepository().getNamespaceAndName(), emptyChangeset.getNode());
pullNewBranchIntoCentralRepository(request, workingCopy);
return Branch.normalBranch(request.getNewBranch(), emptyChangeset.getNode());
}
}
private void checkoutParentBranchIfSpecified(BranchRequest request, com.aragost.javahg.Repository repository) {
if (request.getParentBranch() != null) {
try {
UpdateCommand.on(repository).rev(request.getParentBranch()).execute();
} catch (IOException e) {
throw new InternalRepositoryException(getRepository(), "Could not check out parent branch " + request.getParentBranch(), e);
}
}
}
private Changeset createNewBranchWithEmptyCommit(BranchRequest request, com.aragost.javahg.Repository repository) {
com.aragost.javahg.commands.BranchCommand.on(repository).set(request.getNewBranch());
User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
return CommitCommand
.on(repository)
.user(String.format("%s <%s>", currentUser.getDisplayName(), currentUser.getMail()))
.message("Create new branch " + request.getNewBranch())
.execute();
}
private void pullNewBranchIntoCentralRepository(BranchRequest request, WorkingCopy<com.aragost.javahg.Repository> workingCopy) {
try {
PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
workdirFactory.configure(pullCommand);
pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
} catch (Exception e) {
// TODO handle failed update
throw new IntegrateChangesFromWorkdirException(getRepository(),
String.format("Could not pull new branch '%s' into central repository", request.getNewBranch()),
e);
}
}
}

View File

@@ -53,6 +53,8 @@ public class HgBranchesCommand extends AbstractCommand
implements BranchesCommand implements BranchesCommand
{ {
private static final String DEFAULT_BRANCH_NAME = "default";
/** /**
* Constructs ... * Constructs ...
* *
@@ -88,7 +90,11 @@ public class HgBranchesCommand extends AbstractCommand
node = changeset.getNode(); 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);
}
} }
}); });

View File

@@ -49,6 +49,8 @@ import sonia.scm.web.HgUtil;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
/** /**
* *
@@ -66,7 +68,6 @@ public class HgCommandContext implements Closeable
* Constructs ... * Constructs ...
* *
* *
*
* @param hookManager * @param hookManager
* @param handler * @param handler
* @param repository * @param repository
@@ -84,26 +85,26 @@ public class HgCommandContext implements Closeable
* Constructs ... * Constructs ...
* *
* *
*
* @param hookManager * @param hookManager
* @param hanlder * @param handler
* @param repository * @param repository
* @param directory * @param directory
* @param pending * @param pending
*/ */
public HgCommandContext(HgHookManager hookManager, public HgCommandContext(HgHookManager hookManager,
HgRepositoryHandler hanlder, sonia.scm.repository.Repository repository, HgRepositoryHandler handler, sonia.scm.repository.Repository repository,
File directory, boolean pending) File directory, boolean pending)
{ {
this.hookManager = hookManager; this.hookManager = hookManager;
this.hanlder = hanlder; this.handler = handler;
this.directory = directory; this.directory = directory;
this.scmRepository = repository;
this.encoding = repository.getProperty(PROPERTY_ENCODING); this.encoding = repository.getProperty(PROPERTY_ENCODING);
this.pending = pending; this.pending = pending;
if (Strings.isNullOrEmpty(encoding)) if (Strings.isNullOrEmpty(encoding))
{ {
encoding = hanlder.getConfig().getEncoding(); encoding = handler.getConfig().getEncoding();
} }
} }
@@ -134,13 +135,18 @@ public class HgCommandContext implements Closeable
{ {
if (repository == null) if (repository == null)
{ {
repository = HgUtil.open(hanlder, hookManager, directory, encoding, repository = HgUtil.open(handler, hookManager, directory, encoding, pending);
pending);
} }
return repository; return repository;
} }
public Repository openWithSpecialEnvironment(BiConsumer<sonia.scm.repository.Repository, Map<String, String>> prepareEnvironment)
{
return HgUtil.open(handler, directory, encoding,
pending, environment -> prepareEnvironment.accept(scmRepository, environment));
}
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
/** /**
@@ -151,7 +157,11 @@ public class HgCommandContext implements Closeable
*/ */
public HgConfig getConfig() public HgConfig getConfig()
{ {
return hanlder.getConfig(); return handler.getConfig();
}
public sonia.scm.repository.Repository getScmRepository() {
return scmRepository;
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
@@ -163,7 +173,7 @@ public class HgCommandContext implements Closeable
private String encoding; private String encoding;
/** Field description */ /** Field description */
private HgRepositoryHandler hanlder; private HgRepositoryHandler handler;
/** Field description */ /** Field description */
private HgHookManager hookManager; private HgHookManager hookManager;
@@ -173,4 +183,6 @@ public class HgCommandContext implements Closeable
/** Field description */ /** Field description */
private Repository repository; private Repository repository;
private final sonia.scm.repository.Repository scmRepository;
} }

View File

@@ -126,6 +126,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
return new HgBranchesCommand(context, repository); return new HgBranchesCommand(context, repository);
} }
@Override
public BranchCommand getBranchCommand() {
return new HgBranchCommand(context, repository, handler.getWorkdirFactory());
}
/** /**
* Method description * Method description
* *
@@ -192,6 +197,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
* @return the corresponding {@link ModificationsCommand} implemented from the Plugins * @return the corresponding {@link ModificationsCommand} implemented from the Plugins
* @throws CommandNotSupportedException if there is no Implementation * @throws CommandNotSupportedException if there is no Implementation
*/ */
@Override
public ModificationsCommand getModificationsCommand() { public ModificationsCommand getModificationsCommand() {
return new HgModificationsCommand(context,repository); return new HgModificationsCommand(context,repository);
} }

View File

@@ -0,0 +1,9 @@
package sonia.scm.repository.spi;
import com.aragost.javahg.Repository;
import com.aragost.javahg.commands.PullCommand;
import sonia.scm.repository.util.WorkdirFactory;
public interface HgWorkdirFactory extends WorkdirFactory<Repository, HgCommandContext> {
void configure(PullCommand pullCommand);
}

View File

@@ -0,0 +1,48 @@
package sonia.scm.repository.spi;
import com.aragost.javahg.Repository;
import com.aragost.javahg.commands.CloneCommand;
import com.aragost.javahg.commands.PullCommand;
import sonia.scm.repository.util.SimpleWorkdirFactory;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.BiConsumer;
public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<Repository, HgCommandContext> implements HgWorkdirFactory {
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
@Inject
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder) {
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
}
@Override
public ParentAndClone<Repository> cloneRepository(HgCommandContext context, File target) throws IOException {
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer =
(repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer);
CloneCommand.on(centralRepository).execute(target.getAbsolutePath());
return new ParentAndClone<>(centralRepository, Repository.open(target));
}
@Override
protected void closeRepository(Repository repository) {
repository.close();
}
@Override
protected sonia.scm.repository.Repository getScmRepository(HgCommandContext context) {
return context.getScmRepository();
}
@Override
public void configure(PullCommand pullCommand) {
pullCommand.cmdAppend("--config", "hooks.changegroup.scm=python:scmhooks.postHook");
pullCommand.cmdAppend("--config", "hooks.pretxnchangegroup.scm=python:scmhooks.preHook");
}
}

View File

@@ -34,7 +34,6 @@
package sonia.scm.web; package sonia.scm.web;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -42,16 +41,12 @@ import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.HgConfig; import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgPythonScript; import sonia.scm.repository.HgPythonScript;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryRequestListenerUtil; import sonia.scm.repository.RepositoryRequestListenerUtil;
import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.security.CipherUtil;
import sonia.scm.util.AssertUtil; import sonia.scm.util.AssertUtil;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.cgi.CGIExecutor; import sonia.scm.web.cgi.CGIExecutor;
import sonia.scm.web.cgi.CGIExecutorFactory; import sonia.scm.web.cgi.CGIExecutorFactory;
import sonia.scm.web.cgi.EnvList; import sonia.scm.web.cgi.EnvList;
@@ -62,14 +57,12 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Map;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.util.Base64;
/** /**
* *
@@ -79,25 +72,9 @@ import java.util.Base64;
public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet 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 */ /** Field description */
public static final String ENV_SESSION_PREFIX = "SCM_"; public static final String ENV_SESSION_PREFIX = "SCM_";
/** Field description */
private static final String SCM_CREDENTIALS = "SCM_CREDENTIALS";
/** Field description */ /** Field description */
private static final long serialVersionUID = -3492811300905099810L; private static final long serialVersionUID = -3492811300905099810L;
@@ -107,30 +84,18 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
*
*
* @param cgiExecutorFactory
* @param configuration
* @param handler
* @param hookManager
* @param requestListenerUtil
*/
@Inject @Inject
public HgCGIServlet(CGIExecutorFactory cgiExecutorFactory, public HgCGIServlet(CGIExecutorFactory cgiExecutorFactory,
ScmConfiguration configuration, ScmConfiguration configuration,
HgRepositoryHandler handler, HgHookManager hookManager, HgRepositoryHandler handler,
RepositoryRequestListenerUtil requestListenerUtil) RepositoryRequestListenerUtil requestListenerUtil,
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder)
{ {
this.cgiExecutorFactory = cgiExecutorFactory; this.cgiExecutorFactory = cgiExecutorFactory;
this.configuration = configuration; this.configuration = configuration;
this.handler = handler; this.handler = handler;
this.hookManager = hookManager;
this.requestListenerUtil = requestListenerUtil; this.requestListenerUtil = requestListenerUtil;
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
this.exceptionHandler = new HgCGIExceptionHandler(); this.exceptionHandler = new HgCGIExceptionHandler();
this.command = HgPythonScript.HGWEB.getFile(SCMContext.getContext()); 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 * Method description
* *
@@ -262,7 +193,6 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
HttpServletResponse response, Repository repository) HttpServletResponse response, Repository repository)
throws IOException, ServletException throws IOException, ServletException
{ {
File directory = handler.getDirectory(repository.getId());
CGIExecutor executor = cgiExecutorFactory.createExecutor(configuration, CGIExecutor executor = cgiExecutorFactory.createExecutor(configuration,
getServletContext(), request, response); getServletContext(), request, response);
@@ -271,41 +201,7 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
executor.setExceptionHandler(exceptionHandler); executor.setExceptionHandler(exceptionHandler);
executor.setStatusCodeHandler(exceptionHandler); executor.setStatusCodeHandler(exceptionHandler);
executor.setContentLengthWorkaround(true); executor.setContentLengthWorkaround(true);
executor.getEnvironment().set(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName()); hgRepositoryEnvironmentBuilder.buildFor(repository, request, executor.getEnvironment().asMutableMap());
executor.getEnvironment().set(ENV_REPOSITORY_ID, repository.getId());
executor.getEnvironment().set(ENV_REPOSITORY_PATH,
directory.getAbsolutePath());
// add hook environment
Map<String, String> environment = executor.getEnvironment().asMutableMap();
if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation
// Issue 959: https://goo.gl/zH5eY8
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
}
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
//J-
HgEnvironment.prepareEnvironment(
environment,
handler,
hookManager,
request
);
//J+
addCredentials(executor.getEnvironment(), request);
// unused ???
HttpSession session = request.getSession(false);
if (session != null)
{
passSessionAttributes(executor.getEnvironment(), session);
}
String interpreter = getInterpreter(); String interpreter = getInterpreter();
@@ -358,9 +254,8 @@ public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet
/** Field description */ /** Field description */
private final HgRepositoryHandler handler; private final HgRepositoryHandler handler;
/** Field description */
private final HgHookManager hookManager;
/** Field description */ /** Field description */
private final RepositoryRequestListenerUtil requestListenerUtil; private final RepositoryRequestListenerUtil requestListenerUtil;
private final HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder;
} }

View File

@@ -42,6 +42,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.api.HgHookMessage.Severity;
import sonia.scm.repository.spi.HgHookContextProvider; import sonia.scm.repository.spi.HgHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.security.BearerToken;
import sonia.scm.security.CipherUtil; import sonia.scm.security.CipherUtil;
import sonia.scm.security.Tokens;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -93,7 +94,7 @@ public class HgHookCallbackServlet extends HttpServlet
private static final String PARAM_CHALLENGE = "challenge"; private static final String PARAM_CHALLENGE = "challenge";
/** Field description */ /** Field description */
private static final String PARAM_CREDENTIALS = "credentials"; private static final String PARAM_TOKEN = "token";
/** Field description */ /** Field description */
private static final String PARAM_NODE = "node"; private static final String PARAM_NODE = "node";
@@ -179,11 +180,11 @@ public class HgHookCallbackServlet extends HttpServlet
if (Util.isNotEmpty(node)) 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); 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 try
{ {
credentials = CipherUtil.getInstance().decode(credentials); token = CipherUtil.getInstance().decode(token);
if (Util.isNotEmpty(credentials)) if (Util.isNotEmpty(token))
{
int index = credentials.indexOf(':');
if (index > 0 && index < credentials.length())
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
AuthenticationToken accessToken = createToken(token);
//J- //J-
subject.login( subject.login(accessToken);
Tokens.createAuthenticationToken(
request,
credentials.substring(0, index),
credentials.substring(index + 1)
)
);
//J+
}
else
{
logger.error("could not find delimiter");
}
} }
} }
catch (Exception ex) 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) private void fireHook(HttpServletResponse response, String repositoryId, String node, RepositoryHookType type)
throws IOException throws IOException
{ {

View File

@@ -0,0 +1,56 @@
package sonia.scm.web;
import sonia.scm.repository.HgEnvironment;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.Repository;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.Map;
public class HgRepositoryEnvironmentBuilder {
private static final String ENV_REPOSITORY_NAME = "REPO_NAME";
private static final String ENV_REPOSITORY_PATH = "SCM_REPOSITORY_PATH";
private static final String ENV_REPOSITORY_ID = "SCM_REPOSITORY_ID";
private static final String ENV_PYTHON_HTTPS_VERIFY = "PYTHONHTTPSVERIFY";
private static final String ENV_HTTP_POST_ARGS = "SCM_HTTP_POST_ARGS";
private final HgRepositoryHandler handler;
private final HgHookManager hookManager;
@Inject
public HgRepositoryEnvironmentBuilder(HgRepositoryHandler handler, HgHookManager hookManager) {
this.handler = handler;
this.hookManager = hookManager;
}
public void buildFor(Repository repository, HttpServletRequest request, Map<String, String> environment) {
File directory = handler.getDirectory(repository.getId());
environment.put(ENV_REPOSITORY_NAME, repository.getNamespace() + "/" + repository.getName());
environment.put(ENV_REPOSITORY_ID, repository.getId());
environment.put(ENV_REPOSITORY_PATH,
directory.getAbsolutePath());
// add hook environment
if (handler.getConfig().isDisableHookSSLValidation()) {
// disable ssl validation
// Issue 959: https://goo.gl/zH5eY8
environment.put(ENV_PYTHON_HTTPS_VERIFY, "0");
}
// enable experimental httppostargs protocol of mercurial
// Issue 970: https://goo.gl/poascp
environment.put(ENV_HTTP_POST_ARGS, String.valueOf(handler.getConfig().isEnableHttpPostArgs()));
HgEnvironment.prepareEnvironment(
environment,
handler,
hookManager,
request
);
}
}

View File

@@ -46,6 +46,8 @@ import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgContext; import sonia.scm.repository.HgContext;
import sonia.scm.repository.HgContextProvider; import sonia.scm.repository.HgContextProvider;
import sonia.scm.repository.HgHookManager; 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 // bind servlets
serve(MAPPING_HOOK).with(HgHookCallbackServlet.class); serve(MAPPING_HOOK).with(HgHookCallbackServlet.class);
bind(HgWorkdirFactory.class).to(SimpleHgWorkdirFactory.class);
} }
} }

View File

@@ -58,6 +58,8 @@ import sonia.scm.util.Util;
import java.io.File; import java.io.File;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -103,6 +105,19 @@ public final class HgUtil
*/ */
public static Repository open(HgRepositoryHandler handler, public static Repository open(HgRepositoryHandler handler,
HgHookManager hookManager, File directory, String encoding, boolean pending) HgHookManager hookManager, File directory, String encoding, boolean pending)
{
return open(
handler,
directory,
encoding,
pending,
environment -> HgEnvironment.prepareEnvironment(environment, handler, hookManager)
);
}
public static Repository open(HgRepositoryHandler handler,
File directory, String encoding, boolean pending,
Consumer<Map<String, String>> prepareEnvironment)
{ {
String enc = encoding; String enc = encoding;
@@ -113,8 +128,7 @@ public final class HgUtil
RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT; RepositoryConfiguration repoConfiguration = RepositoryConfiguration.DEFAULT;
HgEnvironment.prepareEnvironment(repoConfiguration.getEnvironment(), prepareEnvironment.accept(repoConfiguration.getEnvironment());
handler, hookManager);
repoConfiguration.addExtension(HgFileviewExtension.class); repoConfiguration.addExtension(HgFileviewExtension.class);
repoConfiguration.setEnablePendingChangesets(pending); repoConfiguration.setEnablePendingChangesets(pending);

View File

@@ -40,7 +40,7 @@ import os, urllib, urllib2
baseUrl = os.environ['SCM_URL'] baseUrl = os.environ['SCM_URL']
challenge = os.environ['SCM_CHALLENGE'] challenge = os.environ['SCM_CHALLENGE']
credentials = os.environ['SCM_CREDENTIALS'] token = os.environ['SCM_BEARER_TOKEN']
repositoryId = os.environ['SCM_REPOSITORY_ID'] repositoryId = os.environ['SCM_REPOSITORY_ID']
def printMessages(ui, msgs): def printMessages(ui, msgs):
@@ -54,13 +54,13 @@ def callHookUrl(ui, repo, hooktype, node):
try: try:
url = baseUrl + hooktype url = baseUrl + hooktype
ui.debug( "send scm-hook to " + url + " and " + node + "\n" ) 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 # open url but ignore proxy settings
proxy_handler = urllib2.ProxyHandler({}) proxy_handler = urllib2.ProxyHandler({})
opener = urllib2.build_opener(proxy_handler) opener = urllib2.build_opener(proxy_handler)
req = urllib2.Request(url, data) req = urllib2.Request(url, data)
conn = opener.open(req) 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" ) ui.debug( "scm-hook " + hooktype + " success with status code " + str(conn.code) + "\n" )
printMessages(ui, conn) printMessages(ui, conn)
abort = False abort = False

View File

@@ -77,7 +77,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override @Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) { 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); handler.init(contextProvider);
HgTestUtil.checkForSkip(handler); HgTestUtil.checkForSkip(handler);
@@ -87,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test @Test
public void getDirectory() { 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 hgConfig = new HgConfig();
hgConfig.setHgBinary("hg"); hgConfig.setHgBinary("hg");

View File

@@ -105,7 +105,7 @@ public final class HgTestUtil
RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver()); RepositoryLocationResolver repositoryLocationResolver = new RepositoryLocationResolver(context, repoDao, new InitialRepositoryLocationResolver());
HgRepositoryHandler handler = HgRepositoryHandler handler =
new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null); new HgRepositoryHandler(new InMemoryConfigurationStoreFactory(), new HgContextProvider(), repositoryLocationResolver, null, null);
Path repoDir = directory.toPath(); Path repoDir = directory.toPath();
when(repoDao.getPath(any())).thenReturn(repoDir); when(repoDao.getPath(any())).thenReturn(repoDir);
handler.init(context); handler.init(context);
@@ -128,6 +128,7 @@ public final class HgTestUtil
"http://localhost:8081/scm/hook/hg/"); "http://localhost:8081/scm/hook/hg/");
when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn( when(hookManager.createUrl(any(HttpServletRequest.class))).thenReturn(
"http://localhost:8081/scm/hook/hg/"); "http://localhost:8081/scm/hook/hg/");
when(hookManager.getCredentials()).thenReturn("");
return hookManager; return hookManager;
} }

View File

@@ -53,7 +53,7 @@ public class HgBranchCommand implements BranchCommand
public Branch branch(String name) throws IOException public Branch branch(String name) throws IOException
{ {
com.aragost.javahg.commands.BranchCommand.on(repository).set(name); com.aragost.javahg.commands.BranchCommand.on(repository).set(name);
return new Branch(name, repository.tip().getNode()); return Branch.normalBranch(name, repository.tip().getNode());
} }
} }

View File

@@ -40,7 +40,6 @@ import org.junit.Before;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.HgTestUtil; import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.RepositoryPathNotFoundException;
import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryTestData;
import sonia.scm.util.MockUtil; import sonia.scm.util.MockUtil;
@@ -77,7 +76,7 @@ public class AbstractHgCommandTestBase extends ZippedRepositoryTestBase
* @throws IOException * @throws IOException
*/ */
@Before @Before
public void initHgHandler() throws IOException, RepositoryPathNotFoundException { public void initHgHandler() throws IOException {
this.handler = HgTestUtil.createHandler(tempFolder.newFolder()); this.handler = HgTestUtil.createHandler(tempFolder.newFolder());
HgTestUtil.checkForSkip(handler); HgTestUtil.checkForSkip(handler);

View File

@@ -0,0 +1,41 @@
package sonia.scm.repository.spi;
import com.aragost.javahg.commands.PullCommand;
import com.google.inject.util.Providers;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import sonia.scm.repository.Branch;
import sonia.scm.repository.HgTestUtil;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
import java.io.IOException;
import java.util.List;
public class HgBranchCommandTest extends AbstractHgCommandTestBase {
@Test
public void shouldCreateBranch() throws IOException {
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder)) {
@Override
public void configure(PullCommand pullCommand) {
// we do not want to configure http hooks in this unit test
}
};
BranchRequest branchRequest = new BranchRequest();
branchRequest.setNewBranch("new_branch");
new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest);
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
}
private List<Branch> readBranches() {
return new HgBranchesCommand(cmdContext, repository).getBranches();
}
}

View File

@@ -1,6 +1,6 @@
// @flow // @flow
import React from "react"; 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 type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
@@ -13,7 +13,8 @@ type Props = {
value?: SelectValue, value?: SelectValue,
placeholder: string, placeholder: string,
loadingMessage: string, loadingMessage: string,
noOptionsMessage: string noOptionsMessage: string,
creatable?: boolean
}; };
@@ -42,11 +43,12 @@ class Autocomplete extends React.Component<Props, State> {
}; };
render() { render() {
const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions } = this.props; const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions, creatable } = this.props;
return ( return (
<div className="field"> <div className="field">
<LabelWithHelpIcon label={label} helpText={helpText} /> <LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control"> <div className="control">
{creatable?
<AsyncCreatable <AsyncCreatable
cacheOptions cacheOptions
loadOptions={loadSuggestions} loadOptions={loadSuggestions}
@@ -63,6 +65,18 @@ class Autocomplete extends React.Component<Props, State> {
}); });
}} }}
/> />
:
<Async
cacheOptions
loadOptions={loadSuggestions}
onChange={this.handleInputChange}
value={value}
placeholder={placeholder}
loadingMessage={() => loadingMessage}
noOptionsMessage={() => noOptionsMessage}
/>
}
</div> </div>
</div> </div>
); );

View File

@@ -19,15 +19,6 @@ class BaseUrlSettings extends React.Component<Props> {
<div> <div>
<Subtitle subtitle={t("base-url-settings.name")} /> <Subtitle subtitle={t("base-url-settings.name")} />
<div className="columns"> <div className="columns">
<div className="column is-half">
<Checkbox
checked={forceBaseUrl}
label={t("base-url-settings.force-base-url")}
onChange={this.handleForceBaseUrlChange}
disabled={!hasUpdatePermission}
helpText={t("help.forceBaseUrlHelpText")}
/>
</div>
<div className="column is-half"> <div className="column is-half">
<InputField <InputField
label={t("base-url-settings.base-url")} label={t("base-url-settings.base-url")}
@@ -37,6 +28,15 @@ class BaseUrlSettings extends React.Component<Props> {
helpText={t("help.baseUrlHelpText")} helpText={t("help.baseUrlHelpText")}
/> />
</div> </div>
<div className="column is-half">
<Checkbox
checked={forceBaseUrl}
label={t("base-url-settings.force-base-url")}
onChange={this.handleForceBaseUrlChange}
disabled={!hasUpdatePermission}
helpText={t("help.forceBaseUrlHelpText")}
/>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,42 @@
package sonia.scm;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Function;
import static java.util.Optional.ofNullable;
import static sonia.scm.group.DisplayGroup.from;
public abstract class GenericDisplayManager<D, T extends ReducedModelObject> implements DisplayManager<T> {
private final GenericDAO<D> dao;
private final Function<D, T> transform;
protected GenericDisplayManager(GenericDAO<D> dao, Function<D, T> transform) {
this.dao = dao;
this.transform = transform;
}
@Override
public Collection<T> autocomplete(String filter) {
checkPermission();
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
return SearchUtil.search(
searchRequest,
dao.getAll(),
object -> matches(searchRequest, object)? transform.apply(object): null
);
}
protected abstract void checkPermission();
protected abstract boolean matches(SearchRequest searchRequest, D object);
@Override
public Optional<T> get(String id) {
return ofNullable(dao.get(id)).map(transform);
}
}

View File

@@ -46,8 +46,10 @@ import sonia.scm.cache.CacheManager;
import sonia.scm.cache.GuavaCacheManager; import sonia.scm.cache.GuavaCacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.group.DefaultGroupDisplayManager;
import sonia.scm.group.DefaultGroupManager; import sonia.scm.group.DefaultGroupManager;
import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupDisplayManager;
import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupManagerProvider; import sonia.scm.group.GroupManagerProvider;
import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.group.xml.XmlGroupDAO;
@@ -102,8 +104,10 @@ import sonia.scm.template.MustacheTemplateEngine;
import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngine;
import sonia.scm.template.TemplateEngineFactory; import sonia.scm.template.TemplateEngineFactory;
import sonia.scm.template.TemplateServlet; import sonia.scm.template.TemplateServlet;
import sonia.scm.user.DefaultUserDisplayManager;
import sonia.scm.user.DefaultUserManager; import sonia.scm.user.DefaultUserManager;
import sonia.scm.user.UserDAO; import sonia.scm.user.UserDAO;
import sonia.scm.user.UserDisplayManager;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.user.UserManagerProvider; import sonia.scm.user.UserManagerProvider;
import sonia.scm.user.xml.XmlUserDAO; import sonia.scm.user.xml.XmlUserDAO;
@@ -268,8 +272,11 @@ public class ScmServletModule extends ServletModule
RepositoryManagerProvider.class); RepositoryManagerProvider.class);
bindDecorated(UserManager.class, DefaultUserManager.class, bindDecorated(UserManager.class, DefaultUserManager.class,
UserManagerProvider.class); UserManagerProvider.class);
bind(UserDisplayManager.class, DefaultUserDisplayManager.class);
bindDecorated(GroupManager.class, DefaultGroupManager.class, bindDecorated(GroupManager.class, DefaultGroupManager.class,
GroupManagerProvider.class); GroupManagerProvider.class);
bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class);
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class); bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);
// bind sslcontext provider // bind sslcontext provider

View File

@@ -0,0 +1,32 @@
package sonia.scm.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import sonia.scm.ExceptionWithContext;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ContextualFallbackExceptionMapper implements ExceptionMapper<ExceptionWithContext> {
private static final Logger logger = LoggerFactory.getLogger(ContextualFallbackExceptionMapper.class);
@Override
public Response toResponse(ExceptionWithContext exception) {
logger.warn("mapping unexpected {} to status code 500", exception.getClass().getName(), exception);
ErrorDto errorDto = new ErrorDto();
errorDto.setMessage(exception.getMessage());
errorDto.setContext(exception.getContext());
errorDto.setErrorCode(exception.getCode());
errorDto.setTransactionId(MDC.get("transaction_id"));
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorDto)
.type(VndMediaType.ERROR_TYPE)
.build();
}
}

View File

@@ -4,8 +4,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.NotEmpty;
import sonia.scm.ReducedModelObject; import sonia.scm.ReducedModelObject;
import sonia.scm.group.GroupManager; import sonia.scm.group.GroupDisplayManager;
import sonia.scm.user.UserManager; import sonia.scm.user.UserDisplayManager;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
@@ -30,14 +30,14 @@ public class AutoCompleteResource {
private ReducedObjectModelToDtoMapper mapper; private ReducedObjectModelToDtoMapper mapper;
private UserManager userManager; private UserDisplayManager userDisplayManager;
private GroupManager groupManager; private GroupDisplayManager groupDisplayManager;
@Inject @Inject
public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserManager userManager, GroupManager groupManager) { public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserDisplayManager userDisplayManager, GroupDisplayManager groupDisplayManager) {
this.mapper = mapper; this.mapper = mapper;
this.userManager = userManager; this.userDisplayManager = userDisplayManager;
this.groupManager = groupManager; this.groupDisplayManager = groupDisplayManager;
} }
@GET @GET
@@ -51,7 +51,7 @@ public class AutoCompleteResource {
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public List<ReducedObjectModelDto> searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) { public List<ReducedObjectModelDto> searchUser(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
return map(userManager.autocomplete(filter)); return map(userDisplayManager.autocomplete(filter));
} }
@GET @GET
@@ -65,7 +65,7 @@ public class AutoCompleteResource {
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public List<ReducedObjectModelDto> searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) { public List<ReducedObjectModelDto> searchGroup(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) {
return map(groupManager.autocomplete(filter)); return map(groupDisplayManager.autocomplete(filter));
} }
private <T extends ReducedModelObject> List<ReducedObjectModelDto> map(Collection<T> autocomplete) { private <T extends ReducedModelObject> List<ReducedObjectModelDto> map(Collection<T> autocomplete) {

View File

@@ -3,9 +3,12 @@ package sonia.scm.api.v2.resources;
import com.google.inject.Inject; import com.google.inject.Inject;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -25,22 +28,36 @@ public class BranchCollectionToDtoMapper {
this.branchToDtoMapper = branchToDtoMapper; this.branchToDtoMapper = branchToDtoMapper;
} }
public HalRepresentation map(String namespace, String name, Collection<Branch> branches) { public HalRepresentation map(Repository repository, Collection<Branch> branches) {
return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches))); return new HalRepresentation(
createLinks(repository),
embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches)));
} }
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) { public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) {
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); 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); String baseUrl = resourceLinks.branchCollection().self(namespace, name);
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo().with(createSelfLink(baseUrl));
.with(Links.linkingTo().self(baseUrl).build()); if (RepositoryPermissions.push(repository).isPermitted()) {
linksBuilder.single(createCreateLink(namespace, name));
}
return linksBuilder.build(); return linksBuilder.build();
} }
private Links createSelfLink(String baseUrl) {
return Links.linkingTo().self(baseUrl).build();
}
private Link createCreateLink(String namespace, String name) {
return Link.link("create", resourceLinks.branch().create(namespace, name));
}
private Embedded embedDtos(List<BranchDto> dtos) { private Embedded embedDtos(List<BranchDto> dtos) {
return embeddedBuilder() return embeddedBuilder()
.with("branches", dtos) .with("branches", dtos)

View File

@@ -6,12 +6,22 @@ import de.otto.edison.hal.Links;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor @Getter @Setter @NoArgsConstructor
public class BranchDto extends HalRepresentation { 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 name;
private String revision; private String revision;
private boolean defaultBranch;
BranchDto(Links links, Embedded embedded) { BranchDto(Links links, Embedded embedded) {
super(links, embedded); super(links, embedded);

View File

@@ -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;
}

View File

@@ -1,9 +1,11 @@
package sonia.scm.api.v2.resources; 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.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.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult; import sonia.scm.PageResult;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.Branches; import sonia.scm.repository.Branches;
@@ -12,21 +14,27 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.BranchCommandBuilder;
import sonia.scm.repository.api.CommandNotSupportedException; import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; 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.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound; import static sonia.scm.NotFoundException.notFound;
@@ -38,12 +46,15 @@ public class BranchRootResource {
private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper; private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
private final ResourceLinks resourceLinks;
@Inject @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.serviceFactory = serviceFactory;
this.branchToDtoMapper = branchToDtoMapper; this.branchToDtoMapper = branchToDtoMapper;
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper; this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper; this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
this.resourceLinks = resourceLinks;
} }
/** /**
@@ -100,12 +111,7 @@ public class BranchRootResource {
@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException { @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
boolean branchExists = repositoryService.getBranchesCommand() if (!branchExists(branchName, repositoryService)){
.getBranches()
.getBranches()
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
if (!branchExists){
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name)); throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
} }
Repository repository = repositoryService.getRepository(); 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. * Returns the branches for a repository.
* *
@@ -141,14 +199,14 @@ public class BranchRootResource {
@ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "branches not supported for given repository"), @ResponseCode(code = 400, condition = "branches not supported for given repository"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), @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 = 404, condition = "not found, no repository found for the given namespace and name"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException { public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Branches branches = repositoryService.getBranchesCommand().getBranches(); 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) { } catch (CommandNotSupportedException ex) {
return Response.status(Response.Status.BAD_REQUEST).build(); return Response.status(Response.Status.BAD_REQUEST).build();
} }

View File

@@ -60,7 +60,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
} }
if (repositoryService.isSupported(Command.BRANCHES)) { if (repositoryService.isSupported(Command.BRANCHES)) {
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, 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))); embeddedBuilder.with("parents", getListOfObjects(source.getParents(), parent -> changesetToParentDtoMapper.map(new Changeset(parent, 0L, null), repository)));

View File

@@ -386,6 +386,9 @@ class ResourceLinks {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href(); return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
} }
public String create(String namespace, String name) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
}
} }
public IncomingLinks incoming() { public IncomingLinks incoming() {

View File

@@ -6,6 +6,7 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService; import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;

View File

@@ -0,0 +1,25 @@
package sonia.scm.group;
import sonia.scm.GenericDisplayManager;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import javax.inject.Inject;
public class DefaultGroupDisplayManager extends GenericDisplayManager<Group, DisplayGroup> implements GroupDisplayManager {
@Inject
public DefaultGroupDisplayManager(GroupDAO groupDAO) {
super(groupDAO, DisplayGroup::from);
}
@Override
protected void checkPermission() {
GroupPermissions.autocomplete().check();
}
@Override
protected boolean matches(SearchRequest searchRequest, Group group) {
return SearchUtil.matchesOne(searchRequest, group.getName(), group.getDescription());
}
}

View File

@@ -195,7 +195,7 @@ public class DefaultGroupManager extends AbstractGroupManager
final PermissionActionCheck<Group> check = GroupPermissions.read(); final PermissionActionCheck<Group> check = GroupPermissions.read();
return SearchUtil.search(searchRequest, groupDAO.getAll(), return SearchUtil.search(searchRequest, groupDAO.getAll(),
new TransformFilter<Group>() new TransformFilter<Group, Group>()
{ {
@Override @Override
public Group accept(Group group) public Group accept(Group group)
@@ -241,13 +241,6 @@ public class DefaultGroupManager extends AbstractGroupManager
return group; return group;
} }
@Override
public Collection<Group> autocomplete(String filter) {
GroupPermissions.autocomplete().check();
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
return SearchUtil.search(searchRequest, groupDAO.getAll(), group -> matches(searchRequest,group)?group:null);
}
/** /**
* Method description * Method description
* *

View File

@@ -0,0 +1,25 @@
package sonia.scm.user;
import sonia.scm.GenericDisplayManager;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import javax.inject.Inject;
public class DefaultUserDisplayManager extends GenericDisplayManager<User, DisplayUser> implements UserDisplayManager {
@Inject
public DefaultUserDisplayManager(UserDAO userDAO) {
super(userDAO, DisplayUser::from);
}
@Override
protected void checkPermission() {
UserPermissions.autocomplete().check();
}
@Override
protected boolean matches(SearchRequest searchRequest, User user) {
return SearchUtil.matchesOne(searchRequest, user.getName(), user.getDisplayName(), user.getMail());
}
}

View File

@@ -212,13 +212,6 @@ public class DefaultUserManager extends AbstractUserManager
fresh.copyProperties(user); fresh.copyProperties(user);
} }
@Override
public Collection<User> autocomplete(String filter) {
UserPermissions.autocomplete().check();
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
return SearchUtil.search(searchRequest, userDAO.getAll(), user -> matches(searchRequest,user)?user:null);
}
/** /**
* Method description * Method description
* *
@@ -236,7 +229,7 @@ public class DefaultUserManager extends AbstractUserManager
} }
final PermissionActionCheck<User> check = UserPermissions.read(); final PermissionActionCheck<User> check = UserPermissions.read();
return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter<User>() { return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter<User, User>() {
@Override @Override
public User accept(User user) public User accept(User user)
{ {
@@ -415,35 +408,6 @@ public class DefaultUserManager extends AbstractUserManager
this.modify(user); 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 --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
private final UserDAO userDAO; private final UserDAO userDAO;

View File

@@ -147,6 +147,10 @@
"3zR9vPNIE1": { "3zR9vPNIE1": {
"displayName": "Ungültige Eingabe", "displayName": "Ungültige Eingabe",
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut." "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": { "namespaceStrategies": {

View File

@@ -147,6 +147,10 @@
"3zR9vPNIE1": { "3zR9vPNIE1": {
"displayName": "Illegal input", "displayName": "Illegal input",
"description": "The values could not be validated. Please correct your input and try again." "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": { "namespaceStrategies": {

View File

@@ -14,16 +14,14 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.Manager; import sonia.scm.DisplayManager;
import sonia.scm.group.DefaultGroupManager; import sonia.scm.group.DefaultGroupDisplayManager;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.user.DefaultUserManager; import sonia.scm.user.DefaultUserDisplayManager;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.xml.XmlUserDAO; import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import sonia.scm.xml.XmlDatabase; import sonia.scm.xml.XmlDatabase;
@@ -51,7 +49,7 @@ public class AutoCompleteResourceTest {
public final ShiroRule shiroRule = new ShiroRule(); public final ShiroRule shiroRule = new ShiroRule();
public static final String URL = "/" + AutoCompleteResource.PATH; 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 Dispatcher dispatcher;
private XmlUserDAO userDao; private XmlUserDAO userDao;
@@ -73,8 +71,8 @@ public class AutoCompleteResourceTest {
XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory); XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory);
groupDao = spy(groupDAO); groupDao = spy(groupDAO);
ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl(); ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl();
UserManager userManager = new DefaultUserManager(this.userDao); DefaultUserDisplayManager userManager = new DefaultUserDisplayManager(this.userDao);
GroupManager groupManager = new DefaultGroupManager(groupDao); DefaultGroupDisplayManager groupManager = new DefaultGroupDisplayManager(groupDao);
AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager); AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager);
dispatcher = createDispatcher(autoCompleteResource); dispatcher = createDispatcher(autoCompleteResource);
} }

View File

@@ -0,0 +1,51 @@
package sonia.scm.api.v2.resources;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BranchDtoTest {
@ParameterizedTest
@ValueSource(strings = {
"v",
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
"val#x",
"val&x",
"val+",
"val,kill",
"val.kill",
"val;kill",
"val<kill",
"val=",
"val>kill",
"val@",
"val]id",
"val`id",
"valid#",
"valid.t",
"val{",
"val{d",
"val{}d",
"val|kill",
"val}"
})
void shouldAcceptValidBranchName(String branchName) {
assertTrue(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
}
@ParameterizedTest
@ValueSource(strings = {
"",
".val",
"val.",
"/val",
"val/",
"val id"
})
void shouldRejectInvalidBranchName(String branchName) {
assertFalse(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
}
}

View File

@@ -25,10 +25,12 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Person; import sonia.scm.repository.Person;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.BranchCommandBuilder;
import sonia.scm.repository.api.BranchesCommandBuilder; import sonia.scm.repository.api.BranchesCommandBuilder;
import sonia.scm.repository.api.LogCommandBuilder; import sonia.scm.repository.api.LogCommandBuilder;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import java.net.URI; import java.net.URI;
import java.time.Instant; 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.anyInt;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.Silent.class) @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_PATH = "space/repo/branches/master";
public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH; public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH;
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); public static final String REVISION = "revision";
private Dispatcher dispatcher;
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@@ -60,6 +65,8 @@ public class BranchRootResourceTest extends RepositoryTestBase {
private RepositoryService service; private RepositoryService service;
@Mock @Mock
private BranchesCommandBuilder branchesCommandBuilder; private BranchesCommandBuilder branchesCommandBuilder;
@Mock
private BranchCommandBuilder branchCommandBuilder;
@Mock @Mock
private LogCommandBuilder logCommandBuilder; private LogCommandBuilder logCommandBuilder;
@@ -89,17 +96,18 @@ public class BranchRootResourceTest extends RepositoryTestBase {
@Before @Before
public void prepareEnvironment() throws Exception { public void prepareEnvironment() {
changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper, resourceLinks);
super.branchRootResource = Providers.of(branchRootResource); 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(new NamespaceAndName("space", "repo"))).thenReturn(service);
when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
when(service.getBranchesCommand()).thenReturn(branchesCommandBuilder); when(service.getBranchesCommand()).thenReturn(branchesCommandBuilder);
when(service.getBranchCommand()).thenReturn(branchCommandBuilder);
when(service.getLogCommand()).thenReturn(logCommandBuilder); when(service.getLogCommand()).thenReturn(logCommandBuilder);
subjectThreadState.bind(); subjectThreadState.bind();
ThreadContext.bind(subject); ThreadContext.bind(subject);
@@ -125,7 +133,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
@Test @Test
public void shouldFindExistingBranch() throws Exception { 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); MockHttpRequest request = MockHttpRequest.get(BRANCH_URL);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
@@ -139,13 +147,12 @@ public class BranchRootResourceTest extends RepositoryTestBase {
@Test @Test
public void shouldFindHistory() throws Exception { public void shouldFindHistory() throws Exception {
String id = "revision_123";
Instant creationDate = Instant.now(); Instant creationDate = Instant.now();
String authorName = "name"; String authorName = "name";
String authorEmail = "em@i.l"; String authorEmail = "em@i.l";
String commit = "my branch commit"; String commit = "my branch commit";
ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class);
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); List<Changeset> changesetList = Lists.newArrayList(new Changeset(REVISION, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
when(changesetPagingResult.getChangesets()).thenReturn(changesetList); when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
when(changesetPagingResult.getTotal()).thenReturn(1); when(changesetPagingResult.getTotal()).thenReturn(1);
when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder);
@@ -153,7 +160,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder);
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);
Branches branches = mock(Branches.class); Branches branches = mock(Branches.class);
List<Branch> branchList = Lists.newArrayList(new Branch("master",id)); List<Branch> branchList = Lists.newArrayList(createBranch("master"));
when(branches.getBranches()).thenReturn(branchList); when(branches.getBranches()).thenReturn(branchList);
when(branchesCommandBuilder.getBranches()).thenReturn(branches); when(branchesCommandBuilder.getBranches()).thenReturn(branches);
MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/"); MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/");
@@ -161,9 +168,85 @@ public class BranchRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
assertEquals(200, response.getStatus()); assertEquals(200, response.getStatus());
log.info("Response :{}", response.getContentAsString()); 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("\"name\":\"%s\"", authorName)));
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); 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);
}
} }

View File

@@ -33,7 +33,7 @@ class BranchToBranchDtoMapperTest {
}); });
mapper.setRegistry(registry); 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")); 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"); assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master");