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
{
int DEFAULT_LIMIT = 5;
/**
* Reloads a object from store and overwrites all changes.

View File

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

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
*/
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);
}
@Override
public Collection<Group> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

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

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,
/**
* @since 2.0
*/
BRANCH,
/**
* @since 1.31
*/

View File

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

View File

@@ -39,6 +39,7 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.Feature;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider;
import java.io.Closeable;
@@ -82,10 +83,9 @@ import java.util.stream.Stream;
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17
*/
@Slf4j
public final class RepositoryService implements Closeable {
private static final Logger logger = LoggerFactory.getLogger(RepositoryService.class);
private static final Logger LOG = LoggerFactory.getLogger(RepositoryService.class);
private final CacheManager cacheManager;
private final PreProcessorUtil preProcessorUtil;
@@ -131,7 +131,7 @@ public final class RepositoryService implements Closeable {
try {
provider.close();
} catch (IOException ex) {
log.error("Could not close repository service provider", ex);
LOG.error("Could not close repository service provider", ex);
}
}
@@ -143,7 +143,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public BlameCommandBuilder getBlameCommand() {
logger.debug("create blame command for repository {}",
LOG.debug("create blame command for repository {}",
repository.getNamespaceAndName());
return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
@@ -158,13 +158,28 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public BranchesCommandBuilder getBranchesCommand() {
logger.debug("create branches command for repository {}",
LOG.debug("create branches command for repository {}",
repository.getNamespaceAndName());
return new BranchesCommandBuilder(cacheManager,
provider.getBranchesCommand(), repository);
}
/**
* The branch command creates new branches.
*
* @return instance of {@link BranchCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
*/
public BranchCommandBuilder getBranchCommand() {
RepositoryPermissions.push(getRepository()).check();
LOG.debug("create branch command for repository {}",
repository.getNamespaceAndName());
return new BranchCommandBuilder(provider.getBranchCommand());
}
/**
* The browse command allows browsing of a repository.
*
@@ -173,7 +188,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public BrowseCommandBuilder getBrowseCommand() {
logger.debug("create browse command for repository {}",
LOG.debug("create browse command for repository {}",
repository.getNamespaceAndName());
return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
@@ -189,7 +204,7 @@ public final class RepositoryService implements Closeable {
* @since 1.43
*/
public BundleCommandBuilder getBundleCommand() {
logger.debug("create bundle command for repository {}",
LOG.debug("create bundle command for repository {}",
repository.getNamespaceAndName());
return new BundleCommandBuilder(provider.getBundleCommand(), repository);
@@ -203,7 +218,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public CatCommandBuilder getCatCommand() {
logger.debug("create cat command for repository {}",
LOG.debug("create cat command for repository {}",
repository.getNamespaceAndName());
return new CatCommandBuilder(provider.getCatCommand());
@@ -218,7 +233,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public DiffCommandBuilder getDiffCommand() {
logger.debug("create diff command for repository {}",
LOG.debug("create diff command for repository {}",
repository.getNamespaceAndName());
return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
@@ -234,7 +249,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31
*/
public IncomingCommandBuilder getIncomingCommand() {
logger.debug("create incoming command for repository {}",
LOG.debug("create incoming command for repository {}",
repository.getNamespaceAndName());
return new IncomingCommandBuilder(cacheManager,
@@ -249,7 +264,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public LogCommandBuilder getLogCommand() {
logger.debug("create log command for repository {}",
LOG.debug("create log command for repository {}",
repository.getNamespaceAndName());
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
@@ -264,7 +279,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public ModificationsCommandBuilder getModificationsCommand() {
logger.debug("create modifications command for repository {}", repository.getNamespaceAndName());
LOG.debug("create modifications command for repository {}", repository.getNamespaceAndName());
return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
}
@@ -277,7 +292,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31
*/
public OutgoingCommandBuilder getOutgoingCommand() {
logger.debug("create outgoing command for repository {}",
LOG.debug("create outgoing command for repository {}",
repository.getNamespaceAndName());
return new OutgoingCommandBuilder(cacheManager,
@@ -293,7 +308,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31
*/
public PullCommandBuilder getPullCommand() {
logger.debug("create pull command for repository {}",
LOG.debug("create pull command for repository {}",
repository.getNamespaceAndName());
return new PullCommandBuilder(provider.getPullCommand(), repository);
@@ -308,7 +323,7 @@ public final class RepositoryService implements Closeable {
* @since 1.31
*/
public PushCommandBuilder getPushCommand() {
logger.debug("create push command for repository {}",
LOG.debug("create push command for repository {}",
repository.getNamespaceAndName());
return new PushCommandBuilder(provider.getPushCommand());
@@ -331,7 +346,7 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
*/
public TagsCommandBuilder getTagsCommand() {
logger.debug("create tags command for repository {}",
LOG.debug("create tags command for repository {}",
repository.getNamespaceAndName());
return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
@@ -347,7 +362,7 @@ public final class RepositoryService implements Closeable {
* @since 1.43
*/
public UnbundleCommandBuilder getUnbundleCommand() {
logger.debug("create unbundle command for repository {}",
LOG.debug("create unbundle command for repository {}",
repository.getNamespaceAndName());
return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
@@ -364,7 +379,8 @@ public final class RepositoryService implements Closeable {
* @since 2.0.0
*/
public MergeCommandBuilder getMergeCommand() {
logger.debug("create merge command for repository {}",
RepositoryPermissions.push(getRepository()).check();
LOG.debug("create merge command for repository {}",
repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand());

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);
}
/**
* Method description
*
*
* @return
*/
public BranchCommand getBranchCommand()
{
throw new CommandNotSupportedException(Command.BRANCH);
}
/**
* 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
*/
public static <T> Collection<T> search(SearchRequest searchRequest,
Collection<T> collection, TransformFilter<T> filter)
public static <T, R> Collection<R> search(SearchRequest searchRequest,
Collection<T> collection, TransformFilter<T, R> filter)
{
List<T> items = new ArrayList<T>();
List<R> items = new ArrayList<>();
int index = 0;
int counter = 0;
Iterator<T> it = collection.iterator();
while (it.hasNext())
{
T item = filter.accept(it.next());
R item = filter.accept(it.next());
if (item != null)
{

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());
}
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<User> autocomplete(String filter);
/**
* Changes the password of the logged in user.
* @param oldPassword The current encrypted password of the user.

View File

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

View File

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

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;
import org.eclipse.jgit.lib.Repository;
import sonia.scm.repository.spi.GitContext;
import sonia.scm.repository.spi.WorkingCopy;
import sonia.scm.repository.util.WorkdirFactory;
public interface GitWorkdirFactory {
WorkingCopy createWorkingCopy(GitContext gitContext);
public interface GitWorkdirFactory extends WorkdirFactory<Repository, GitContext> {
}

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

View File

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

View File

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

View File

@@ -4,64 +4,49 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.util.SimpleWorkdirFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class SimpleGitWorkdirFactory implements GitWorkdirFactory {
private static final Logger logger = LoggerFactory.getLogger(SimpleGitWorkdirFactory.class);
private final File poolDirectory;
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
public SimpleGitWorkdirFactory() {
this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
}
public SimpleGitWorkdirFactory(File poolDirectory) {
this.poolDirectory = poolDirectory;
poolDirectory.mkdirs();
SimpleGitWorkdirFactory(File poolDirectory) {
super(poolDirectory);
}
public WorkingCopy createWorkingCopy(GitContext gitContext) {
@Override
public ParentAndClone<Repository> cloneRepository(GitContext context, File target) {
try {
Repository clone = cloneRepository(gitContext.getDirectory(), createNewWorkdir());
return new WorkingCopy(clone, this::close);
} 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))
return new ParentAndClone<>(null, Git.cloneRepository()
.setURI(createScmTransportProtocolUri(context.getDirectory()))
.setDirectory(target)
.call()
.getRepository();
.getRepository());
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e);
}
}
private String createScmTransportProtocolUri(File bareRepository) {
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();
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;
import org.junit.Test;
import sonia.scm.repository.util.CloseableWrapper;
import java.util.function.Consumer;
@@ -11,19 +12,20 @@ public class CloseableWrapperTest {
@Test
public void shouldExecuteGivenMethodAtClose() {
Consumer<String> wrapped = new Consumer<String>() {
Consumer<AutoCloseable> wrapped = new Consumer<AutoCloseable>() {
// no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy
@Override
public void accept(String s) {
public void accept(AutoCloseable s) {
}
};
Consumer<String> closer = spy(wrapped);
Consumer<AutoCloseable> closer = spy(wrapped);
try (CloseableWrapper<String> wrapper = new CloseableWrapper<>("test", closer)) {
AutoCloseable autoCloseable = () -> {};
try (CloseableWrapper<AutoCloseable> wrapper = new CloseableWrapper<>(autoCloseable, closer)) {
// nothing to do here
}
verify(closer).accept("test");
verify(closer).accept(autoCloseable);
}
}

View File

@@ -72,7 +72,7 @@ public class GitBranchCommand implements BranchCommand
try
{
Ref ref = git.branchCreate().setName(name).call();
return new Branch(name, GitUtil.getId(ref.getObjectId()));
return Branch.normalBranch(name, GitUtil.getId(ref.getObjectId()));
}
catch (GitAPIException ex)
{

View File

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

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
public ShiroRule shiro = new ShiroRule();
private ScmTransportProtocol scmTransportProtocol;
@Before
public void bindScmProtocol() {
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
RepositoryManager repositoryManager = mock(RepositoryManager.class);
HookEventFacade hookEventFacade = new HookEventFacade(of(repositoryManager), hookContextFactory);
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
scmTransportProtocol = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler));
Transport.register(scmTransportProtocol);
when(gitRepositoryHandler.getRepositoryId(any())).thenReturn("1");
when(repositoryManager.get("1")).thenReturn(new sonia.scm.repository.Repository());
}
@After
public void unregisterScmProtocol() {
Transport.unregister(scmTransportProtocol);
}
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Test
public void shouldDetectMergeableBranches() {

View File

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

View File

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

View File

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

View File

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

View File

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

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
{
private static final String DEFAULT_BRANCH_NAME = "default";
/**
* Constructs ...
*
@@ -88,7 +90,11 @@ public class HgBranchesCommand extends AbstractCommand
node = changeset.getNode();
}
return new Branch(hgBranch.getName(), node);
if (DEFAULT_BRANCH_NAME.equals(hgBranch.getName())) {
return Branch.defaultBranch(hgBranch.getName(), node);
} else {
return Branch.normalBranch(hgBranch.getName(), node);
}
}
});

View File

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

View File

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

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

View File

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

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.HgContextProvider;
import sonia.scm.repository.HgHookManager;
import sonia.scm.repository.spi.HgWorkdirFactory;
import sonia.scm.repository.spi.SimpleHgWorkdirFactory;
/**
*
@@ -81,5 +83,7 @@ public class HgServletModule extends ServletModule
// bind servlets
serve(MAPPING_HOOK).with(HgHookCallbackServlet.class);
bind(HgWorkdirFactory.class).to(SimpleHgWorkdirFactory.class);
}
}

View File

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

View File

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

View File

@@ -77,7 +77,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null);
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver, null, null);
handler.init(contextProvider);
HgTestUtil.checkForSkip(handler);
@@ -87,7 +87,7 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null);
HgRepositoryHandler repositoryHandler = new HgRepositoryHandler(factory, provider, locationResolver, null, null);
HgConfig hgConfig = new HgConfig();
hgConfig.setHgBinary("hg");

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

@@ -6,12 +6,22 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor
public class BranchDto extends HalRepresentation {
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
private String name;
private String revision;
private boolean defaultBranch;
BranchDto(Links links, Embedded embedded) {
super(links, embedded);

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;
import com.google.common.base.Strings;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Branches;
@@ -12,21 +14,27 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.BranchCommandBuilder;
import sonia.scm.repository.api.CommandNotSupportedException;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
@@ -38,12 +46,15 @@ public class BranchRootResource {
private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
private final ResourceLinks resourceLinks;
@Inject
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper, ResourceLinks resourceLinks) {
this.serviceFactory = serviceFactory;
this.branchToDtoMapper = branchToDtoMapper;
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
this.resourceLinks = resourceLinks;
}
/**
@@ -100,12 +111,7 @@ public class BranchRootResource {
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
boolean branchExists = repositoryService.getBranchesCommand()
.getBranches()
.getBranches()
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
if (!branchExists){
if (!branchExists(branchName, repositoryService)){
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
}
Repository repository = repositoryService.getRepository();
@@ -125,6 +131,58 @@ public class BranchRootResource {
}
}
/**
* Creates a new branch.
*
* @param namespace the namespace of the repository
* @param name the name of the repository
* @param branchRequest the request giving the name of the new branch and an optional parent branch
* @return A response with the link to the new branch (if created successfully).
*/
@POST
@Path("")
@Consumes(VndMediaType.BRANCH_REQUEST)
@StatusCodes({
@ResponseCode(code = 201, condition = "create success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"push\" privilege"),
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch"))
public Response create(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@Valid BranchRequestDto branchRequest) throws IOException {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
String branchName = branchRequest.getName();
String parentName = branchRequest.getParent();
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
if (branchExists(branchName, repositoryService)) {
throw alreadyExists(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
}
Repository repository = repositoryService.getRepository();
RepositoryPermissions.push(repository).check();
BranchCommandBuilder branchCommand = repositoryService.getBranchCommand();
if (!Strings.isNullOrEmpty(parentName)) {
if (!branchExists(parentName, repositoryService)) {
throw notFound(entity(Branch.class, parentName).in(Repository.class, namespace + "/" + name));
}
branchCommand.from(parentName);
}
Branch newBranch = branchCommand.branch(branchName);
return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build();
}
}
private boolean branchExists(String branchName, RepositoryService repositoryService) throws IOException {
return repositoryService.getBranchesCommand()
.getBranches()
.getBranches()
.stream()
.anyMatch(branch -> branchName.equals(branch.getName()));
}
/**
* Returns the branches for a repository.
*
@@ -141,14 +199,14 @@ public class BranchRootResource {
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "branches not supported for given repository"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"read repository\" privilege"),
@ResponseCode(code = 404, condition = "not found, no repository found for the given namespace and name"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
Branches branches = repositoryService.getBranchesCommand().getBranches();
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
return Response.ok(branchCollectionToDtoMapper.map(repositoryService.getRepository(), branches.getBranches())).build();
} catch (CommandNotSupportedException ex) {
return Response.status(Response.Status.BAD_REQUEST).build();
}

View File

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

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();
}
public String create(String namespace, String name) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
}
}
public IncomingLinks incoming() {

View File

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

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

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);
}
@Override
public Collection<User> autocomplete(String filter) {
UserPermissions.autocomplete().check();
SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT);
return SearchUtil.search(searchRequest, userDAO.getAll(), user -> matches(searchRequest,user)?user:null);
}
/**
* Method description
*
@@ -236,7 +229,7 @@ public class DefaultUserManager extends AbstractUserManager
}
final PermissionActionCheck<User> check = UserPermissions.read();
return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter<User>() {
return SearchUtil.search(searchRequest, userDAO.getAll(), new TransformFilter<User, User>() {
@Override
public User accept(User user)
{
@@ -415,35 +408,6 @@ public class DefaultUserManager extends AbstractUserManager
this.modify(user);
}
/**
* Method description
*
*
* @param unmarshaller
* @param path
*/
private void createDefaultAccount(Unmarshaller unmarshaller, String path)
{
InputStream input = DefaultUserManager.class.getResourceAsStream(path);
try
{
User user = (User) unmarshaller.unmarshal(input);
user.setType(userDAO.getType());
user.setCreationDate(System.currentTimeMillis());
userDAO.add(user);
}
catch (Exception ex)
{
logger.error("could not create account", ex);
}
finally
{
IOUtil.close(input);
}
}
//~--- fields ---------------------------------------------------------------
private final UserDAO userDAO;

View File

@@ -147,6 +147,10 @@
"3zR9vPNIE1": {
"displayName": "Ungültige Eingabe",
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut."
},
"CHRM7IQzo1": {
"displayName": "Änderung fehlgeschlagen",
"description": "Die Änderung ist fehlgeschlagen. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise."
}
},
"namespaceStrategies": {

View File

@@ -147,6 +147,10 @@
"3zR9vPNIE1": {
"displayName": "Illegal input",
"description": "The values could not be validated. Please correct your input and try again."
},
"CHRM7IQzo1": {
"displayName": "Change failed",
"description": "The change failed. Please contact your administrator for further assistance."
}
},
"namespaceStrategies": {

View File

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

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

View File

@@ -33,7 +33,7 @@ class BranchToBranchDtoMapperTest {
});
mapper.setRegistry(registry);
Branch branch = new Branch("master", "42");
Branch branch = Branch.normalBranch("master", "42");
BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold"));
assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master");