Merged 2.0.0-m3

This commit is contained in:
Philipp Czora
2018-11-14 14:31:17 +01:00
239 changed files with 3495 additions and 5560 deletions

View File

@@ -485,6 +485,7 @@
see https://blogs.oracle.com/darcy/entry/bootclasspath_older_source see https://blogs.oracle.com/darcy/entry/bootclasspath_older_source
--> -->
<compilerArgument>-Xlint:unchecked,-options</compilerArgument> <compilerArgument>-Xlint:unchecked,-options</compilerArgument>
<compilerArgument>-parameters</compilerArgument>
</configuration> </configuration>
</plugin> </plugin>
@@ -757,7 +758,7 @@
<guice.version>4.0</guice.version> <guice.version>4.0</guice.version>
<!-- event bus --> <!-- event bus -->
<legman.version>1.4.0</legman.version> <legman.version>1.4.2</legman.version>
<!-- webserver --> <!-- webserver -->
<jetty.version>9.2.10.v20150310</jetty.version> <jetty.version>9.2.10.v20150310</jetty.version>

View File

@@ -0,0 +1,15 @@
package sonia.scm.security;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Use this annotation to mark REST resource methods that may be accessed <b>without</b> authentication.
* To mark all methods of a complete class you can annotate the class instead.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowAnonymousAccess {
}

View File

@@ -1,11 +1,34 @@
package sonia.scm; package sonia.scm;
public class AlreadyExistsException extends Exception { import java.util.List;
public AlreadyExistsException(String message) { import static java.util.Collections.singletonList;
super(message); import static java.util.stream.Collectors.joining;
public class AlreadyExistsException extends ExceptionWithContext {
private static final String CODE = "FtR7UznKU1";
public AlreadyExistsException(ModelObject object) {
this(singletonList(new ContextEntry(object.getClass(), object.getId())));
} }
public AlreadyExistsException() { public static AlreadyExistsException alreadyExists(ContextEntry.ContextBuilder builder) {
return new AlreadyExistsException(builder.build());
}
private AlreadyExistsException(List<ContextEntry> context) {
super(context, createMessage(context));
}
@Override
public String getCode() {
return CODE;
}
private static String createMessage(List<ContextEntry> context) {
return context.stream()
.map(c -> c.getType().toLowerCase() + " with id " + c.getId())
.collect(joining(" in ", "", " already exists"));
} }
} }

View File

@@ -1,4 +1,34 @@
package sonia.scm; package sonia.scm;
public class ConcurrentModificationException extends Exception { import java.util.Collections;
import java.util.List;
import static java.util.stream.Collectors.joining;
public class ConcurrentModificationException extends ExceptionWithContext {
private static final String CODE = "2wR7UzpPG1";
public ConcurrentModificationException(Class type, String id) {
this(Collections.singletonList(new ContextEntry(type, id)));
}
public ConcurrentModificationException(String type, String id) {
this(Collections.singletonList(new ContextEntry(type, id)));
}
private ConcurrentModificationException(List<ContextEntry> context) {
super(context, createMessage(context));
}
@Override
public String getCode() {
return CODE;
}
private static String createMessage(List<ContextEntry> context) {
return context.stream()
.map(c -> c.getType().toLowerCase() + " with id " + c.getId())
.collect(joining(" in ", "", " has been modified concurrently"));
}
} }

View File

@@ -0,0 +1,84 @@
package sonia.scm;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.util.AssertUtil;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class ContextEntry {
private final String type;
private final String id;
ContextEntry(Class type, String id) {
this(type.getSimpleName(), id);
}
ContextEntry(String type, String id) {
AssertUtil.assertIsNotEmpty(type);
AssertUtil.assertIsNotEmpty(id);
this.type = type;
this.id = id;
}
public String getType() {
return type;
}
public String getId() {
return id;
}
public static class ContextBuilder {
private final List<ContextEntry> context = new LinkedList<>();
public static List<ContextEntry> noContext() {
return new ContextBuilder().build();
}
public static List<ContextEntry> only(String type, String id) {
return new ContextBuilder().in(type, id).build();
}
public static ContextBuilder entity(Repository repository) {
return new ContextBuilder().in(repository.getNamespaceAndName());
}
public static ContextBuilder entity(NamespaceAndName namespaceAndName) {
return new ContextBuilder().in(Repository.class, namespaceAndName.logString());
}
public static ContextBuilder entity(Class type, String id) {
return new ContextBuilder().in(type, id);
}
public static ContextBuilder entity(String type, String id) {
return new ContextBuilder().in(type, id);
}
public ContextBuilder in(Repository repository) {
return in(repository.getNamespaceAndName());
}
public ContextBuilder in(NamespaceAndName namespaceAndName) {
return this.in(Repository.class, namespaceAndName.logString());
}
public ContextBuilder in(Class type, String id) {
context.add(new ContextEntry(type, id));
return this;
}
public ContextBuilder in(String type, String id) {
context.add(new ContextEntry(type, id));
return this;
}
public List<ContextEntry> build() {
return Collections.unmodifiableList(context);
}
}
}

View File

@@ -0,0 +1,26 @@
package sonia.scm;
import java.util.List;
import static java.util.Collections.unmodifiableList;
public abstract class ExceptionWithContext extends RuntimeException {
private final List<ContextEntry> context;
public ExceptionWithContext(List<ContextEntry> context, String message) {
super(message);
this.context = context;
}
public ExceptionWithContext(List<ContextEntry> context, String message, Exception cause) {
super(message, cause);
this.context = context;
}
public List<ContextEntry> getContext() {
return unmodifiableList(context);
}
public abstract String getCode();
}

View File

@@ -54,25 +54,21 @@ public interface HandlerBase<T extends TypedObject>
* *
* @return The persisted object. * @return The persisted object.
*/ */
T create(T object) throws AlreadyExistsException; T create(T object);
/** /**
* Removes a persistent object. * Removes a persistent object.
* *
* *
* @param object to delete * @param object to delete
*
* @throws IOException
*/ */
void delete(T object) throws NotFoundException; void delete(T object);
/** /**
* Modifies a persistent object. * Modifies a persistent object.
* *
* *
* @param object to modify * @param object to modify
*
* @throws IOException
*/ */
void modify(T object) throws NotFoundException; void modify(T object);
} }

View File

@@ -58,7 +58,7 @@ public interface Manager<T extends ModelObject>
* *
* @throws NotFoundException * @throws NotFoundException
*/ */
void refresh(T object) throws NotFoundException; void refresh(T object);
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -66,7 +66,7 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
} }
@Override @Override
public T create(T object) throws AlreadyExistsException { public T create(T object) {
return decorated.create(object); return decorated.create(object);
} }

View File

@@ -1,10 +1,38 @@
package sonia.scm; package sonia.scm;
public class NotFoundException extends RuntimeException { import java.util.Collections;
public NotFoundException(String type, String id) { import java.util.List;
super(type + " with id '" + id + "' not found");
import static java.util.stream.Collectors.joining;
public class NotFoundException extends ExceptionWithContext {
private static final String CODE = "AGR7UzkhA1";
public NotFoundException(Class type, String id) {
this(Collections.singletonList(new ContextEntry(type, id)));
} }
public NotFoundException() { public NotFoundException(String type, String id) {
this(Collections.singletonList(new ContextEntry(type, id)));
}
public static NotFoundException notFound(ContextEntry.ContextBuilder contextBuilder) {
return new NotFoundException(contextBuilder.build());
}
private NotFoundException(List<ContextEntry> context) {
super(context, createMessage(context));
}
@Override
public String getCode() {
return CODE;
}
private static String createMessage(List<ContextEntry> context) {
return context.stream()
.map(c -> c.getType().toLowerCase() + " with id " + c.getId())
.collect(joining(" in ", "could not find ", ""));
} }
} }

View File

@@ -33,33 +33,30 @@
package sonia.scm; package sonia.scm;
import java.util.Collections;
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @version 1.6 * @version 1.6
*/ */
public class NotSupportedFeatuerException extends Exception public class NotSupportedFeatureException extends ExceptionWithContext {
{
/** Field description */
private static final long serialVersionUID = 256498734456613496L; private static final long serialVersionUID = 256498734456613496L;
//~--- constructors --------------------------------------------------------- private static final String CODE = "9SR8G0kmU1";
/** public NotSupportedFeatureException(String feature)
* Constructs ...
*
*/
public NotSupportedFeatuerException() {}
/**
* Constructs ...
*
*
* @param message
*/
public NotSupportedFeatuerException(String message)
{ {
super(message); super(Collections.emptyList(),createMessage(feature));
}
@Override
public String getCode() {
return CODE;
}
private static String createMessage(String feature) {
return "feature " + feature + " is not supported by this repository";
} }
} }

View File

@@ -45,28 +45,12 @@ public final class Filters
/** Field description */ /** Field description */
public static final String PATTERN_ALL = "/*"; public static final String PATTERN_ALL = "/*";
/** Field description */
public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
/** Field description */ /** Field description */
public static final String PATTERN_DEBUG = "/debug.html"; public static final String PATTERN_DEBUG = "/debug.html";
/** Field description */
public static final String PATTERN_GROUPS = REST_API_PATH + "/groups*";
/** Field description */
public static final String PATTERN_PLUGINS = REST_API_PATH + "/plugins*";
/** Field description */
public static final String PATTERN_RESOURCE_REGEX =
"^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)";
/** Field description */ /** Field description */
public static final String PATTERN_RESTAPI = REST_API_PATH + "/*"; public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
/** Field description */
public static final String PATTERN_USERS = REST_API_PATH + "/users*";
/** authentication priority */ /** authentication priority */
public static final int PRIORITY_AUTHENTICATION = 5000; public static final int PRIORITY_AUTHENTICATION = 5000;

View File

@@ -37,7 +37,6 @@ package sonia.scm.repository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.repository.ImportResult.Builder; import sonia.scm.repository.ImportResult.Builder;
import java.io.File; import java.io.File;
@@ -243,7 +242,7 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler
*/ */
private void importRepository(RepositoryManager manager, private void importRepository(RepositoryManager manager,
String repositoryName) String repositoryName)
throws IOException, AlreadyExistsException { throws IOException {
Repository repository = Repository repository =
createRepository(getRepositoryDirectory(repositoryName), repositoryName); createRepository(getRepositoryDirectory(repositoryName), repositoryName);

View File

@@ -38,7 +38,7 @@ package sonia.scm.repository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotSupportedFeatuerException; import sonia.scm.NotSupportedFeatureException;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
@@ -165,13 +165,12 @@ public abstract class AbstractRepositoryHandler<C extends RepositoryConfig>
* *
* @return * @return
* *
* @throws NotSupportedFeatuerException * @throws NotSupportedFeatureException
*/ */
@Override @Override
public ImportHandler getImportHandler() throws NotSupportedFeatuerException public ImportHandler getImportHandler() throws NotSupportedFeatureException
{ {
throw new NotSupportedFeatuerException( throw new NotSupportedFeatureException("import");
"import handler is not supported by this repository handler");
} }
/** /**

View File

@@ -40,6 +40,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.ConfigurationException; import sonia.scm.ConfigurationException;
import sonia.scm.ContextEntry;
import sonia.scm.io.CommandResult; import sonia.scm.io.CommandResult;
import sonia.scm.io.ExtendedCommand; import sonia.scm.io.ExtendedCommand;
import sonia.scm.io.FileSystem; import sonia.scm.io.FileSystem;
@@ -81,14 +82,14 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
} }
@Override @Override
public Repository create(Repository repository) throws AlreadyExistsException { public Repository create(Repository repository) {
File directory = getDirectory(repository); File directory = getDirectory(repository);
if (directory.exists()) { if (directory.exists()) {
throw new AlreadyExistsException(); throw new AlreadyExistsException(repository);
} }
checkPath(directory); checkPath(directory, repository);
try { try {
fileSystem.create(directory); fileSystem.create(directory);
@@ -128,7 +129,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
try { try {
fileSystem.destroy(directory); fileSystem.destroy(directory);
} catch (IOException e) { } catch (IOException e) {
throw new InternalRepositoryException("could not delete repository directory", e); throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("directory", directory.toString()).in(repository), "could not delete repository directory", e);
} }
cleanupEmptyDirectories(config.getRepositoryDirectory(), cleanupEmptyDirectories(config.getRepositoryDirectory(),
directory.getParentFile()); directory.getParentFile());
@@ -201,7 +202,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
} }
protected void create(Repository repository, File directory) protected void create(Repository repository, File directory)
throws IOException, AlreadyExistsException { throws IOException {
ExtendedCommand cmd = buildCreateCommand(repository, directory); ExtendedCommand cmd = buildCreateCommand(repository, directory);
CommandResult result = cmd.execute(); CommandResult result = cmd.execute();
@@ -256,9 +257,9 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
* Check path for existing repositories * Check path for existing repositories
* *
* @param directory repository target directory * @param directory repository target directory
* @throws AlreadyExistsException * @throws RuntimeException when the parent directory already is a repository
*/ */
private void checkPath(File directory) throws AlreadyExistsException { private void checkPath(File directory, Repository repository) {
File repositoryDirectory = config.getRepositoryDirectory(); File repositoryDirectory = config.getRepositoryDirectory();
File parent = directory.getParentFile(); File parent = directory.getParentFile();
@@ -266,9 +267,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
logger.trace("check {} for existing repository", parent); logger.trace("check {} for existing repository", parent);
if (isRepository(parent)) { if (isRepository(parent)) {
logger.error("parent path {} is a repository", parent); throw new InternalRepositoryException(repository, "parent path " + parent + " is a repository");
throw new AlreadyExistsException();
} }
parent = parent.getParentFile(); parent = parent.getParentFile();

View File

@@ -1,15 +1,34 @@
package sonia.scm.repository; package sonia.scm.repository;
public class InternalRepositoryException extends RuntimeException { import sonia.scm.ContextEntry;
public InternalRepositoryException(Throwable ex) { import sonia.scm.ExceptionWithContext;
super(ex);
import java.util.List;
public class InternalRepositoryException extends ExceptionWithContext {
public InternalRepositoryException(ContextEntry.ContextBuilder context, String message) {
this(context, message, null);
} }
public InternalRepositoryException(String msg, Exception ex) { public InternalRepositoryException(ContextEntry.ContextBuilder context, String message, Exception cause) {
super(msg, ex); this(context.build(), message, cause);
} }
public InternalRepositoryException(String message) { public InternalRepositoryException(Repository repository, String message) {
super(message); this(ContextEntry.ContextBuilder.entity(repository), message, null);
}
public InternalRepositoryException(Repository repository, String message, Exception cause) {
this(ContextEntry.ContextBuilder.entity(repository), message, cause);
}
public InternalRepositoryException(List<ContextEntry> context, String message, Exception cause) {
super(context, message, cause);
}
@Override
public String getCode() {
return null;
} }
} }

View File

@@ -25,9 +25,13 @@ public class NamespaceAndName implements Comparable<NamespaceAndName> {
return name; return name;
} }
public String logString() {
return getNamespace() + "/" + getName();
}
@Override @Override
public String toString() { public String toString() {
return getNamespace() + "/" + getName(); return logString();
} }
@Override @Override

View File

@@ -1,84 +0,0 @@
/**
* 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;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.NotFoundException;
import sonia.scm.util.Util;
/**
* Signals that the specified path could be found.
*
* @author Sebastian Sdorra
*/
public class PathNotFoundException extends NotFoundException
{
/** Field description */
private static final long serialVersionUID = 4629690181172951809L;
//~--- constructors ---------------------------------------------------------
/**
* Constructs a new {@link PathNotFoundException}
* with the specified path.
*
*
* @param path path which could not be found
*/
public PathNotFoundException(String path)
{
super("path", Util.nonNull(path));
this.path = Util.nonNull(path);
}
//~--- get methods ----------------------------------------------------------
/**
* Return the path which could not be found.
*
*
* @return path which could not be found
*/
public String getPath()
{
return path;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String path;
}

View File

@@ -37,7 +37,6 @@ import com.github.sdorra.ssp.PermissionObject;
import com.github.sdorra.ssp.StaticPermissions; import com.github.sdorra.ssp.StaticPermissions;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware; import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject; import sonia.scm.ModelObject;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -50,8 +49,11 @@ import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlTransient;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Source code repository. * Source code repository.
@@ -79,7 +81,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified; private Long lastModified;
private String namespace; private String namespace;
private String name; private String name;
private List<Permission> permissions; private final Set<Permission> permissions = new HashSet<>();
@XmlElement(name = "public") @XmlElement(name = "public")
private boolean publicReadable = false; private boolean publicReadable = false;
private boolean archived = false; private boolean archived = false;
@@ -127,7 +129,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name; this.name = name;
this.contact = contact; this.contact = contact;
this.description = description; this.description = description;
this.permissions = Lists.newArrayList();
if (Util.isNotEmpty(permissions)) { if (Util.isNotEmpty(permissions)) {
this.permissions.addAll(Arrays.asList(permissions)); this.permissions.addAll(Arrays.asList(permissions));
@@ -200,12 +201,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return new NamespaceAndName(getNamespace(), getName()); return new NamespaceAndName(getNamespace(), getName());
} }
public List<Permission> getPermissions() { public Collection<Permission> getPermissions() {
if (permissions == null) { return Collections.unmodifiableCollection(permissions);
permissions = Lists.newArrayList();
}
return permissions;
} }
/** /**
@@ -300,8 +297,17 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name; this.name = name;
} }
public void setPermissions(List<Permission> permissions) { public void setPermissions(Collection<Permission> permissions) {
this.permissions = permissions; this.permissions.clear();
this.permissions.addAll(permissions);
}
public void addPermission(Permission newPermission) {
this.permissions.add(newPermission);
}
public void removePermission(Permission permission) {
this.permissions.remove(permission);
} }
public void setPublicReadable(boolean publicReadable) { public void setPublicReadable(boolean publicReadable) {

View File

@@ -36,7 +36,7 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.Handler; import sonia.scm.Handler;
import sonia.scm.NotSupportedFeatuerException; import sonia.scm.NotSupportedFeatureException;
import sonia.scm.plugin.ExtensionPoint; import sonia.scm.plugin.ExtensionPoint;
/** /**
@@ -70,9 +70,9 @@ public interface RepositoryHandler
* @return {@link ImportHandler} for the repository type of this handler * @return {@link ImportHandler} for the repository type of this handler
* @since 1.12 * @since 1.12
* *
* @throws NotSupportedFeatuerException * @throws NotSupportedFeatureException
*/ */
public ImportHandler getImportHandler() throws NotSupportedFeatuerException; public ImportHandler getImportHandler() throws NotSupportedFeatureException;
/** /**
* Returns informations about the version of the RepositoryHandler. * Returns informations about the version of the RepositoryHandler.

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.AlreadyExistsException;
import sonia.scm.TypeManager; import sonia.scm.TypeManager;
import java.io.IOException; import java.io.IOException;
@@ -73,7 +72,7 @@ public interface RepositoryManager
* *
* @throws IOException * @throws IOException
*/ */
public void importRepository(Repository repository) throws IOException, AlreadyExistsException; public void importRepository(Repository repository) throws IOException;
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.AlreadyExistsException;
import sonia.scm.ManagerDecorator; import sonia.scm.ManagerDecorator;
import sonia.scm.Type; import sonia.scm.Type;
@@ -82,7 +81,7 @@ public class RepositoryManagerDecorator
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void importRepository(Repository repository) throws IOException, AlreadyExistsException { public void importRepository(Repository repository) throws IOException {
decorated.importRepository(repository); decorated.importRepository(repository);
} }

View File

@@ -1,68 +0,0 @@
/**
* 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;
import sonia.scm.NotFoundException;
/**
* Signals that the specified {@link Repository} could be found.
*
* @author Sebastian Sdorra
* @since 1.6
*/
public class RepositoryNotFoundException extends NotFoundException
{
private static final long serialVersionUID = -6583078808900520166L;
private static final String TYPE_REPOSITORY = "repository";
//~--- constructors ---------------------------------------------------------
/**
* Constructs a new {@link RepositoryNotFoundException} with null as its
* error detail message.
*
*/
public RepositoryNotFoundException(Repository repository) {
super(TYPE_REPOSITORY, repository.getName() + "/" + repository.getNamespace());
}
public RepositoryNotFoundException(String repositoryId) {
super(TYPE_REPOSITORY, repositoryId);
}
public RepositoryNotFoundException(NamespaceAndName namespaceAndName) {
super(TYPE_REPOSITORY, namespaceAndName.toString());
}
}

View File

@@ -1,83 +0,0 @@
/**
* 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;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.NotFoundException;
import sonia.scm.util.Util;
/**
* Signals that the specified revision could be found.
*
* @author Sebastian Sdorra
*/
public class RevisionNotFoundException extends NotFoundException {
/** Field description */
private static final long serialVersionUID = -5594008535358811998L;
//~--- constructors ---------------------------------------------------------
/**
* Constructs a new {@link RevisionNotFoundException}
* with the specified revision.
*
*
* @param revision revision which could not be found
*/
public RevisionNotFoundException(String revision)
{
super("revision", revision);
this.revision = Util.nonNull(revision);
}
//~--- get methods ----------------------------------------------------------
/**
* Return the revision which could not be found.
*
*
* @return revision which could not be found
*/
public String getRevision()
{
return revision;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String revision;
}

View File

@@ -38,7 +38,6 @@ package sonia.scm.repository.api;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
@@ -46,7 +45,6 @@ import sonia.scm.repository.FileObject;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey; import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.BrowseCommand; import sonia.scm.repository.spi.BrowseCommand;
import sonia.scm.repository.spi.BrowseCommandRequest; import sonia.scm.repository.spi.BrowseCommandRequest;
@@ -136,7 +134,7 @@ public final class BrowseCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public BrowserResult getBrowserResult() throws IOException, NotFoundException { public BrowserResult getBrowserResult() throws IOException {
BrowserResult result = null; BrowserResult result = null;
if (disableCache) if (disableCache)

View File

@@ -37,9 +37,7 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.CatCommand; import sonia.scm.repository.spi.CatCommand;
import sonia.scm.repository.spi.CatCommandRequest; import sonia.scm.repository.spi.CatCommandRequest;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
@@ -107,7 +105,7 @@ public final class CatCommandBuilder
* @param outputStream output stream for the content * @param outputStream output stream for the content
* @param path file path * @param path file path
*/ */
public void retriveContent(OutputStream outputStream, String path) throws IOException, PathNotFoundException, RevisionNotFoundException { public void retriveContent(OutputStream outputStream, String path) throws IOException {
getCatResult(outputStream, path); getCatResult(outputStream, path);
} }
@@ -116,7 +114,7 @@ public final class CatCommandBuilder
* *
* @param path file path * @param path file path
*/ */
public InputStream getStream(String path) throws IOException, PathNotFoundException, RevisionNotFoundException { public InputStream getStream(String path) throws IOException {
Preconditions.checkArgument(!Strings.isNullOrEmpty(path), Preconditions.checkArgument(!Strings.isNullOrEmpty(path),
"path is required"); "path is required");
@@ -139,7 +137,7 @@ public final class CatCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public String getContent(String path) throws IOException, PathNotFoundException, RevisionNotFoundException { public String getContent(String path) throws IOException {
String content = null; String content = null;
ByteArrayOutputStream baos = null; ByteArrayOutputStream baos = null;
@@ -186,7 +184,7 @@ public final class CatCommandBuilder
* @throws IOException * @throws IOException
*/ */
private void getCatResult(OutputStream outputStream, String path) private void getCatResult(OutputStream outputStream, String path)
throws IOException, PathNotFoundException, RevisionNotFoundException { throws IOException {
Preconditions.checkNotNull(outputStream, "OutputStream is required"); Preconditions.checkNotNull(outputStream, "OutputStream is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(path), Preconditions.checkArgument(!Strings.isNullOrEmpty(path),
"path is required"); "path is required");

View File

@@ -66,6 +66,10 @@ public enum Command
/** /**
* @since 2.0 * @since 2.0
*/ */
MODIFICATIONS MODIFICATIONS,
/**
* @since 2.0
*/
MERGE
} }

View File

@@ -38,7 +38,6 @@ package sonia.scm.repository.api;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.DiffCommand; import sonia.scm.repository.spi.DiffCommand;
import sonia.scm.repository.spi.DiffCommandRequest; import sonia.scm.repository.spi.DiffCommandRequest;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
@@ -104,7 +103,7 @@ public final class DiffCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException { public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException {
getDiffResult(outputStream); getDiffResult(outputStream);
return this; return this;
@@ -119,7 +118,7 @@ public final class DiffCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public String getContent() throws IOException, RevisionNotFoundException { public String getContent() throws IOException {
String content = null; String content = null;
ByteArrayOutputStream baos = null; ByteArrayOutputStream baos = null;
@@ -199,7 +198,7 @@ public final class DiffCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
private void getDiffResult(OutputStream outputStream) throws IOException, RevisionNotFoundException { private void getDiffResult(OutputStream outputStream) throws IOException {
Preconditions.checkNotNull(outputStream, "OutputStream is required"); Preconditions.checkNotNull(outputStream, "OutputStream is required");
Preconditions.checkArgument(request.isValid(), Preconditions.checkArgument(request.isValid(),
"path and/or revision is required"); "path and/or revision is required");

View File

@@ -46,7 +46,6 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey; import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.LogCommand; import sonia.scm.repository.spi.LogCommand;
import sonia.scm.repository.spi.LogCommandRequest; import sonia.scm.repository.spi.LogCommandRequest;
@@ -165,7 +164,7 @@ public final class LogCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public Changeset getChangeset(String id) throws IOException, RevisionNotFoundException { public Changeset getChangeset(String id) throws IOException {
Changeset changeset; Changeset changeset;
if (disableCache) if (disableCache)
@@ -224,7 +223,7 @@ public final class LogCommandBuilder
* *
* @throws IOException * @throws IOException
*/ */
public ChangesetPagingResult getChangesets() throws IOException, RevisionNotFoundException { public ChangesetPagingResult getChangesets() throws IOException {
ChangesetPagingResult cpr; ChangesetPagingResult cpr;
if (disableCache) if (disableCache)

View File

@@ -0,0 +1,143 @@
package sonia.scm.repository.api;
import com.google.common.base.Preconditions;
import sonia.scm.repository.Person;
import sonia.scm.repository.spi.MergeCommand;
import sonia.scm.repository.spi.MergeCommandRequest;
/**
* Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if
* the branches could be merged without conflicts ({@link #dryRun()}). To do so, you have to specify the name of
* the target branch ({@link #setTargetBranch(String)}) and the name of the branch that should be merged
* ({@link #setBranchToMerge(String)}). Additionally you can specify an author that should be used for the commit
* ({@link #setAuthor(Person)}) and a message template ({@link #setMessageTemplate(String)}) if you are not doing a dry
* run only. If no author is specified, the logged in user and a default message will be used instead.
*
* To actually merge <code>feature_branch</code> into <code>integration_branch</code> do this:
* <pre><code>
* repositoryService.gerMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .executeMerge();
* </code></pre>
*
* If the merge is successful, the result will look like this:
* <pre><code>
* O <- Merge result (new head of integration_branch)
* |\
* | \
* old integration_branch -> O O <- feature_branch
* | |
* O O
* </code></pre>
*
* To check whether they can be merged without conflicts beforehand do this:
* <pre><code>
* repositoryService.gerMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .dryRun()
* .isMergeable();
* </code></pre>
*
* Keep in mind that you should <em>always</em> check the result of a merge even though you may have done a dry run
* beforehand, because the branches can change between the dry run and the actual merge.
*
* @since 2.0.0
*/
public class MergeCommandBuilder {
private final MergeCommand mergeCommand;
private final MergeCommandRequest request = new MergeCommandRequest();
MergeCommandBuilder(MergeCommand mergeCommand) {
this.mergeCommand = mergeCommand;
}
/**
* Use this to set the branch that should be merged into the target branch.
*
* <b>This is mandatory.</b>
*
* @return This builder instance.
*/
public MergeCommandBuilder setBranchToMerge(String branchToMerge) {
request.setBranchToMerge(branchToMerge);
return this;
}
/**
* Use this to set the target branch the other branch should be merged into.
*
* <b>This is mandatory.</b>
*
* @return This builder instance.
*/
public MergeCommandBuilder setTargetBranch(String targetBranch) {
request.setTargetBranch(targetBranch);
return this;
}
/**
* Use this to set the author of the merge commit manually. If this is omitted, the currently logged in user will be
* used instead.
*
* This is optional and for {@link #executeMerge()} only.
*
* @return This builder instance.
*/
public MergeCommandBuilder setAuthor(Person author) {
request.setAuthor(author);
return this;
}
/**
* Use this to set a template for the commit message. If no message is set, a default message will be used.
*
* You can use the placeholder <code>{0}</code> for the branch to be merged and <code>{1}</code> for the target
* branch, eg.:
*
* <pre><code>
* ...setMessageTemplate("Merge of {0} into {1}")...
* </code></pre>
*
* This is optional and for {@link #executeMerge()} only.
*
* @return This builder instance.
*/
public MergeCommandBuilder setMessageTemplate(String messageTemplate) {
request.setMessageTemplate(messageTemplate);
return this;
}
/**
* Use this to reset the command.
* @return This builder instance.
*/
public MergeCommandBuilder reset() {
request.reset();
return this;
}
/**
* Use this to actually do the merge. If an automatic merge is not possible, {@link MergeCommandResult#isSuccess()}
* will return <code>false</code>.
*
* @return The result of the merge.
*/
public MergeCommandResult executeMerge() {
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
return mergeCommand.merge(request);
}
/**
* Use this to check whether the given branches can be merged autmatically. If this is possible,
* {@link MergeDryRunCommandResult#isMergeable()} will return <code>true</code>.
*
* @return The result whether the given branches can be merged automatically.
*/
public MergeDryRunCommandResult dryRun() {
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
return mergeCommand.dryRun(request);
}
}

View File

@@ -0,0 +1,44 @@
package sonia.scm.repository.api;
import java.util.Collection;
import java.util.HashSet;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
/**
* This class keeps the result of a merge of branches. Use {@link #isSuccess()} to check whether the merge was
* sucessfully executed. If the result is <code>false</code> the merge could not be done without conflicts. In this
* case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts.
*/
public class MergeCommandResult {
private final Collection<String> filesWithConflict;
private MergeCommandResult(Collection<String> filesWithConflict) {
this.filesWithConflict = filesWithConflict;
}
public static MergeCommandResult success() {
return new MergeCommandResult(emptyList());
}
public static MergeCommandResult failure(Collection<String> filesWithConflict) {
return new MergeCommandResult(new HashSet<>(filesWithConflict));
}
/**
* If this returns <code>true</code>, the merge was successfull. If this returns <code>false</code> there were
* merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged.
*/
public boolean isSuccess() {
return filesWithConflict.isEmpty();
}
/**
* If the merge was not successful ({@link #isSuccess()} returns <code>false</code>) this will give you a list of
* file paths that could not be merged automatically.
*/
public Collection<String> getFilesWithConflict() {
return unmodifiableCollection(filesWithConflict);
}
}

View File

@@ -0,0 +1,22 @@
package sonia.scm.repository.api;
/**
* This class keeps the result of a merge dry run. Use {@link #isMergeable()} to check whether an automatic merge is
* possible or not.
*/
public class MergeDryRunCommandResult {
private final boolean mergeable;
public MergeDryRunCommandResult(boolean mergeable) {
this.mergeable = mergeable;
}
/**
* This will return <code>true</code>, when an automatic merge is possible <em>at the moment</em>; <code>false</code>
* otherwise.
*/
public boolean isMergeable() {
return mergeable;
}
}

View File

@@ -13,7 +13,6 @@ import sonia.scm.repository.Modifications;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey; import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.ModificationsCommand; import sonia.scm.repository.spi.ModificationsCommand;
import sonia.scm.repository.spi.ModificationsCommandRequest; import sonia.scm.repository.spi.ModificationsCommandRequest;
@@ -67,7 +66,7 @@ public final class ModificationsCommandBuilder {
return this; return this;
} }
public Modifications getModifications() throws IOException, RevisionNotFoundException { public Modifications getModifications() throws IOException {
Modifications modifications; Modifications modifications;
if (disableCache) { if (disableCache) {
log.info("Get modifications for {} with disabled cache", request); log.info("Get modifications for {} with disabled cache", request);

View File

@@ -79,6 +79,7 @@ import java.util.stream.Stream;
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder * @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder * @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder * @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17 * @since 1.17
*/ */
@Slf4j @Slf4j
@@ -353,6 +354,22 @@ public final class RepositoryService implements Closeable {
repository); repository);
} }
/**
* The merge command executes a merge of two branches. It is possible to do a dry run to check, whether the given
* branches can be merged without conflicts.
*
* @return instance of {@link MergeCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.0.0
*/
public MergeCommandBuilder gerMergeCommand() {
logger.debug("create unbundle command for repository {}",
repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand());
}
/** /**
* Returns true if the command is supported by the repository service. * Returns true if the command is supported by the repository service.
* *

View File

@@ -45,6 +45,7 @@ import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.HandlerEventType; import sonia.scm.HandlerEventType;
import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
@@ -57,7 +58,6 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKeyPredicate; import sonia.scm.repository.RepositoryCacheKeyPredicate;
import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.spi.RepositoryServiceProvider;
import sonia.scm.repository.spi.RepositoryServiceResolver; import sonia.scm.repository.spi.RepositoryServiceResolver;
@@ -65,6 +65,9 @@ import sonia.scm.security.ScmSecurityException;
import java.util.Set; import java.util.Set;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
@@ -161,7 +164,7 @@ public final class RepositoryServiceFactory
* @return a implementation of RepositoryService * @return a implementation of RepositoryService
* for the given type of repository * for the given type of repository
* *
* @throws RepositoryNotFoundException if no repository * @throws NotFoundException if no repository
* with the given id is available * with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service * @throws RepositoryServiceNotFoundException if no repository service
* implementation for this kind of repository is available * implementation for this kind of repository is available
@@ -169,7 +172,7 @@ public final class RepositoryServiceFactory
* @throws ScmSecurityException if current user has not read permissions * @throws ScmSecurityException if current user has not read permissions
* for that repository * for that repository
*/ */
public RepositoryService create(String repositoryId) throws RepositoryNotFoundException { public RepositoryService create(String repositoryId) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(repositoryId), Preconditions.checkArgument(!Strings.isNullOrEmpty(repositoryId),
"a non empty repositoryId is required"); "a non empty repositoryId is required");
@@ -177,7 +180,7 @@ public final class RepositoryServiceFactory
if (repository == null) if (repository == null)
{ {
throw new RepositoryNotFoundException(repositoryId); throw new NotFoundException(Repository.class, repositoryId);
} }
return create(repository); return create(repository);
@@ -192,7 +195,7 @@ public final class RepositoryServiceFactory
* @return a implementation of RepositoryService * @return a implementation of RepositoryService
* for the given type of repository * for the given type of repository
* *
* @throws RepositoryNotFoundException if no repository * @throws NotFoundException if no repository
* with the given id is available * with the given id is available
* @throws RepositoryServiceNotFoundException if no repository service * @throws RepositoryServiceNotFoundException if no repository service
* implementation for this kind of repository is available * implementation for this kind of repository is available
@@ -201,7 +204,6 @@ public final class RepositoryServiceFactory
* for that repository * for that repository
*/ */
public RepositoryService create(NamespaceAndName namespaceAndName) public RepositoryService create(NamespaceAndName namespaceAndName)
throws RepositoryNotFoundException
{ {
Preconditions.checkArgument(namespaceAndName != null, Preconditions.checkArgument(namespaceAndName != null,
"a non empty namespace and name is required"); "a non empty namespace and name is required");
@@ -210,7 +212,7 @@ public final class RepositoryServiceFactory
if (repository == null) if (repository == null)
{ {
throw new RepositoryNotFoundException(namespaceAndName); throw notFound(entity(namespaceAndName));
} }
return create(repository); return create(repository);

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import java.io.IOException; import java.io.IOException;
@@ -60,5 +59,5 @@ public interface BrowseCommand
* *
* @throws IOException * @throws IOException
*/ */
BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException, NotFoundException; BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException;
} }

View File

@@ -33,9 +33,6 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@@ -47,7 +44,7 @@ import java.io.OutputStream;
*/ */
public interface CatCommand { public interface CatCommand {
void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RevisionNotFoundException, PathNotFoundException; void getCatResult(CatCommandRequest request, OutputStream output) throws IOException;
InputStream getCatResultStream(CatCommandRequest request) throws IOException, RevisionNotFoundException, PathNotFoundException; InputStream getCatResultStream(CatCommandRequest request) throws IOException;
} }

View File

@@ -33,8 +33,6 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@@ -56,5 +54,5 @@ public interface DiffCommand
* @throws IOException * @throws IOException
* @throws RuntimeException * @throws RuntimeException
*/ */
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException, RevisionNotFoundException; public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException;
} }

View File

@@ -40,10 +40,12 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent; import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -71,18 +73,18 @@ public final class HookEventFacade
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
public HookEventHandler handle(String id) throws RepositoryNotFoundException { public HookEventHandler handle(String id) {
return handle(repositoryManagerProvider.get().get(id)); return handle(repositoryManagerProvider.get().get(id));
} }
public HookEventHandler handle(NamespaceAndName namespaceAndName) throws RepositoryNotFoundException { public HookEventHandler handle(NamespaceAndName namespaceAndName) {
return handle(repositoryManagerProvider.get().get(namespaceAndName)); return handle(repositoryManagerProvider.get().get(namespaceAndName));
} }
public HookEventHandler handle(Repository repository) throws RepositoryNotFoundException { public HookEventHandler handle(Repository repository) {
if (repository == null) if (repository == null)
{ {
throw new RepositoryNotFoundException(repository); throw notFound(entity(repository));
} }
return new HookEventHandler(repositoryManagerProvider.get(), return new HookEventHandler(repositoryManagerProvider.get(),

View File

@@ -37,7 +37,6 @@ package sonia.scm.repository.spi;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
@@ -50,7 +49,7 @@ import java.io.IOException;
*/ */
public interface LogCommand { public interface LogCommand {
Changeset getChangeset(String id) throws IOException, RevisionNotFoundException; Changeset getChangeset(String id) throws IOException;
ChangesetPagingResult getChangesets(LogCommandRequest request) throws IOException, RevisionNotFoundException; ChangesetPagingResult getChangesets(LogCommandRequest request) throws IOException;
} }

View File

@@ -0,0 +1,10 @@
package sonia.scm.repository.spi;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
public interface MergeCommand {
MergeCommandResult merge(MergeCommandRequest request);
MergeDryRunCommandResult dryRun(MergeCommandRequest request);
}

View File

@@ -0,0 +1,93 @@
package sonia.scm.repository.spi;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import sonia.scm.Validateable;
import sonia.scm.repository.Person;
import sonia.scm.util.Util;
import java.io.Serializable;
public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable {
private static final long serialVersionUID = -2650236557922431528L;
private String branchToMerge;
private String targetBranch;
private Person author;
private String messageTemplate;
public String getBranchToMerge() {
return branchToMerge;
}
public void setBranchToMerge(String branchToMerge) {
this.branchToMerge = branchToMerge;
}
public String getTargetBranch() {
return targetBranch;
}
public void setTargetBranch(String targetBranch) {
this.targetBranch = targetBranch;
}
public Person getAuthor() {
return author;
}
public void setAuthor(Person author) {
this.author = author;
}
public String getMessageTemplate() {
return messageTemplate;
}
public void setMessageTemplate(String messageTemplate) {
this.messageTemplate = messageTemplate;
}
public boolean isValid() {
return !Strings.isNullOrEmpty(getBranchToMerge())
&& !Strings.isNullOrEmpty(getTargetBranch());
}
public void reset() {
this.setBranchToMerge(null);
this.setTargetBranch(null);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final MergeCommandRequest other = (MergeCommandRequest) obj;
return Objects.equal(branchToMerge, other.branchToMerge)
&& Objects.equal(targetBranch, other.targetBranch)
&& Objects.equal(author, other.author);
}
@Override
public int hashCode() {
return Objects.hashCode(branchToMerge, targetBranch, author);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("branchToMerge", branchToMerge)
.add("targetBranch", targetBranch)
.add("author", author)
.toString();
}
}

View File

@@ -32,7 +32,6 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import sonia.scm.repository.Modifications; import sonia.scm.repository.Modifications;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException; import java.io.IOException;
@@ -46,8 +45,8 @@ import java.io.IOException;
*/ */
public interface ModificationsCommand { public interface ModificationsCommand {
Modifications getModifications(String revision) throws IOException, RevisionNotFoundException; Modifications getModifications(String revision) throws IOException;
Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException; Modifications getModifications(ModificationsCommandRequest request) throws IOException;
} }

View File

@@ -251,4 +251,12 @@ public abstract class RepositoryServiceProvider implements Closeable
{ {
throw new CommandNotSupportedException(Command.UNBUNDLE); throw new CommandNotSupportedException(Command.UNBUNDLE);
} }
/**
* @since 2.0
*/
public MergeCommand getMergeCommand()
{
throw new CommandNotSupportedException(Command.MERGE);
}
} }

View File

@@ -1,11 +1,19 @@
package sonia.scm.user; package sonia.scm.user;
public class ChangePasswordNotAllowedException extends RuntimeException { import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
public class ChangePasswordNotAllowedException extends ExceptionWithContext {
private static final String CODE = "9BR7qpDAe1";
public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password"; public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password";
public ChangePasswordNotAllowedException(String type) { public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String type) {
super(String.format(WRONG_USER_TYPE, type)); super(context.build(), String.format(WRONG_USER_TYPE, type));
} }
@Override
public String getCode() {
return CODE;
}
} }

View File

@@ -1,8 +1,18 @@
package sonia.scm.user; package sonia.scm.user;
public class InvalidPasswordException extends RuntimeException { import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
public InvalidPasswordException() { public class InvalidPasswordException extends ExceptionWithContext {
super("The given Password does not match with the stored one.");
private static final String CODE = "8YR7aawFW1";
public InvalidPasswordException(ContextEntry.ContextBuilder context) {
super(context.build(), "The given old password does not match with the stored one.");
}
@Override
public String getCode() {
return CODE;
} }
} }

View File

@@ -44,6 +44,7 @@ public class VndMediaType {
public static final String ME = PREFIX + "me" + SUFFIX; public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX;
public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX;
private VndMediaType() { private VndMediaType() {
} }

View File

@@ -301,7 +301,7 @@ public class AuthenticationFilter extends HttpFilter
} }
} }
chain.doFilter(new SecurityHttpServletRequestWrapper(request, username), chain.doFilter(new PropagatePrincipleServletRequestWrapper(request, username),
response); response);
} }

View File

@@ -38,37 +38,17 @@ package sonia.scm.web.filter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletRequestWrapper;
/** public class PropagatePrincipleServletRequestWrapper extends HttpServletRequestWrapper {
*
* @author Sebastian Sdorra
*/
public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper
{
/** private final String principal;
* Constructs ...
* public PropagatePrincipleServletRequestWrapper(HttpServletRequest request, String principal) {
*
* @param request
* @param principal
*/
public SecurityHttpServletRequestWrapper(HttpServletRequest request,
String principal)
{
super(request); super(request);
this.principal = principal; this.principal = principal;
} }
//~--- get methods ----------------------------------------------------------
@Override @Override
public String getRemoteUser() public String getRemoteUser() {
{
return principal; return principal;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
private final String principal;
} }

View File

@@ -43,7 +43,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupNames; import sonia.scm.group.GroupNames;
@@ -132,7 +131,7 @@ public class SyncingRealmHelperTest {
* @throws IOException * @throws IOException
*/ */
@Test @Test
public void testStoreGroupCreate() throws AlreadyExistsException { public void testStoreGroupCreate() {
Group group = new Group("unit-test", "heartOfGold"); Group group = new Group("unit-test", "heartOfGold");
helper.store(group); helper.store(group);
@@ -143,7 +142,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(Group)}. * Tests {@link SyncingRealmHelper#store(Group)}.
*/ */
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testStoreGroupFailure() throws AlreadyExistsException { public void testStoreGroupFailure() {
Group group = new Group("unit-test", "heartOfGold"); Group group = new Group("unit-test", "heartOfGold");
doThrow(AlreadyExistsException.class).when(groupManager).create(group); doThrow(AlreadyExistsException.class).when(groupManager).create(group);
@@ -169,7 +168,7 @@ public class SyncingRealmHelperTest {
* @throws IOException * @throws IOException
*/ */
@Test @Test
public void testStoreUserCreate() throws AlreadyExistsException { public void testStoreUserCreate() {
User user = new User("tricia"); User user = new User("tricia");
helper.store(user); helper.store(user);
@@ -180,7 +179,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(User)} with a thrown {@link AlreadyExistsException}. * Tests {@link SyncingRealmHelper#store(User)} with a thrown {@link AlreadyExistsException}.
*/ */
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testStoreUserFailure() throws AlreadyExistsException { public void testStoreUserFailure() {
User user = new User("tricia"); User user = new User("tricia");
doThrow(AlreadyExistsException.class).when(userManager).create(user); doThrow(AlreadyExistsException.class).when(userManager).create(user);

View File

@@ -0,0 +1,26 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import org.junit.Test;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.it.utils.ScmRequests;
import static org.junit.Assert.assertEquals;
public class AnonymousAccessITCase {
@Test
public void shouldAccessIndexResourceWithoutAuthentication() {
ScmRequests.start()
.requestIndexResource()
.assertStatusCode(200);
}
@Test
public void shouldRejectUserResourceWithoutAuthentication() {
assertEquals(401, RestAssured.given()
.when()
.get(RestUtil.REST_BASE_URL.resolve("users/"))
.statusCode());
}
}

View File

@@ -32,6 +32,7 @@
package sonia.scm.it; package sonia.scm.it;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.assertj.core.api.Assertions;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -49,8 +50,10 @@ import sonia.scm.web.VndMediaType;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
@@ -72,7 +75,7 @@ public class PermissionsITCase {
public TemporaryFolder temporaryFolder = new TemporaryFolder(); public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final String repositoryType; private final String repositoryType;
private int createdPermissions; private Collection<String> createdPermissions;
public PermissionsITCase(String repositoryType) { public PermissionsITCase(String repositoryType) {
@@ -94,7 +97,7 @@ public class PermissionsITCase {
TestData.createNotAdminUser(USER_OWNER, USER_PASS); TestData.createNotAdminUser(USER_OWNER, USER_PASS);
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
TestData.createNotAdminUser(USER_OTHER, USER_PASS); TestData.createNotAdminUser(USER_OTHER, USER_PASS);
createdPermissions = 3; createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
} }
@Test @Test
@@ -131,8 +134,8 @@ public class PermissionsITCase {
@Test @Test
public void ownerShouldSeePermissions() { public void ownerShouldSeePermissions() {
List<Object> userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); List<Map> userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType);
assertEquals(userPermissions.size(), createdPermissions); Assertions.assertThat(userPermissions).extracting(e -> e.get("name")).containsAll(createdPermissions);
} }
@Test @Test

View File

@@ -38,6 +38,10 @@ public class ScmRequests {
return new ScmRequests(); return new ScmRequests();
} }
public IndexResponse requestIndexResource() {
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
}
public IndexResponse requestIndexResource(String username, String password) { public IndexResponse requestIndexResource(String username, String password) {
setUsername(username); setUsername(username);
setPassword(password); setPassword(password);

View File

@@ -99,10 +99,10 @@ public class TestData {
; ;
} }
public static List<Object> getUserPermissions(String username, String password, String repositoryType) { public static List<Map> getUserPermissions(String username, String password, String repositoryType) {
return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK)
.extract() .extract()
.body().jsonPath().getList("_embedded.permissions"); .body().jsonPath().<Map>getList("_embedded.permissions");
} }
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) {

View File

@@ -115,12 +115,6 @@
<artifactId>smp-maven-plugin</artifactId> <artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-3</version> <version>1.0.0-alpha-3</version>
<extensions>true</extensions> <extensions>true</extensions>
<configuration>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration>
</plugin> </plugin>
<plugin> <plugin>

View File

@@ -52,6 +52,10 @@
<extensions>true</extensions> <extensions>true</extensions>
<configuration> <configuration>
<corePlugin>true</corePlugin> <corePlugin>true</corePlugin>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -1,124 +0,0 @@
/**
* 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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/**
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("config/repositories/git")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public class GitConfigResource
{
/**
* Constructs ...
*
*
*
* @param repositoryHandler
*/
@Inject
public GitConfigResource(GitRepositoryHandler repositoryHandler)
{
this.repositoryHandler = repositoryHandler;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@GET
public GitConfig getConfig()
{
GitConfig config = repositoryHandler.getConfig();
if (config == null)
{
config = new GitConfig();
repositoryHandler.setConfig(config);
}
return config;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response setConfig(@Context UriInfo uriInfo, GitConfig config)
{
repositoryHandler.setConfig(config);
repositoryHandler.storeConfig();
return Response.created(uriInfo.getRequestUri()).build();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private GitRepositoryHandler repositoryHandler;
}

View File

@@ -0,0 +1,25 @@
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

@@ -91,6 +91,8 @@ public class GitRepositoryHandler
private final Scheduler scheduler; private final Scheduler scheduler;
private final GitWorkdirFactory workdirFactory;
private Task task; private Task task;
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@@ -104,10 +106,11 @@ public class GitRepositoryHandler
* @param scheduler * @param scheduler
*/ */
@Inject @Inject
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler) public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, GitWorkdirFactory workdirFactory)
{ {
super(storeFactory, fileSystem); super(storeFactory, fileSystem);
this.scheduler = scheduler; this.scheduler = scheduler;
this.workdirFactory = workdirFactory;
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -234,4 +237,8 @@ public class GitRepositoryHandler
{ {
return new File(directory, DIRECTORY_REFS).exists(); return new File(directory, DIRECTORY_REFS).exists();
} }
public GitWorkdirFactory getWorkdirFactory() {
return workdirFactory;
}
} }

View File

@@ -56,6 +56,7 @@ import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import sonia.scm.web.GitUserAgentProvider; import sonia.scm.web.GitUserAgentProvider;
@@ -204,7 +205,7 @@ public final class GitUtil
} }
catch (GitAPIException ex) catch (GitAPIException ex)
{ {
throw new InternalRepositoryException("could not fetch", ex); throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("remote", directory.toString()).in(remoteRepository), "could not fetch", ex);
} }
} }

View File

@@ -0,0 +1,8 @@
package sonia.scm.repository;
import sonia.scm.repository.spi.GitContext;
import sonia.scm.repository.spi.WorkingCopy;
public interface GitWorkdirFactory {
WorkingCopy createWorkingCopy(GitContext gitContext);
}

View File

@@ -160,7 +160,7 @@ public abstract class AbstractGitIncomingOutgoingCommand
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new InternalRepositoryException("could not execute incoming command", ex); throw new InternalRepositoryException(repository, "could not execute incoming command", ex);
} }
finally finally
{ {
@@ -200,13 +200,7 @@ public abstract class AbstractGitIncomingOutgoingCommand
{ {
if (e.getKey().startsWith(prefix)) if (e.getKey().startsWith(prefix))
{ {
if (ref != null)
{
throw new InternalRepositoryException("could not find remote branch");
}
ref = e.getValue(); ref = e.getValue();
break; break;
} }
} }

View File

@@ -114,7 +114,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new InternalRepositoryException("could not execute push/pull command", ex); throw new InternalRepositoryException(repository, "could not execute push/pull command", ex);
} }
return counter; return counter;

View File

@@ -55,6 +55,8 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
@@ -108,9 +110,8 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand
if (gitBlameResult == null) if (gitBlameResult == null)
{ {
throw new InternalRepositoryException( throw new InternalRepositoryException(entity("path", request.getPath()).in(repository),
"could not create blame result for path ".concat( "could not create blame result for path");
request.getPath()));
} }
List<BlameLine> blameLines = new ArrayList<BlameLine>(); List<BlameLine> blameLines = new ArrayList<BlameLine>();
@@ -150,7 +151,7 @@ public class GitBlameCommand extends AbstractGitCommand implements BlameCommand
} }
catch (GitAPIException ex) catch (GitAPIException ex)
{ {
throw new InternalRepositoryException("could not create blame view", ex); throw new InternalRepositoryException(repository, "could not create blame view", ex);
} }
return result; return result;

View File

@@ -102,7 +102,7 @@ public class GitBranchesCommand extends AbstractGitCommand
} }
catch (GitAPIException ex) catch (GitAPIException ex)
{ {
throw new InternalRepositoryException("could not read branches", ex); throw new InternalRepositoryException(repository, "could not read branches", ex);
} }
return branches; return branches;

View File

@@ -55,9 +55,7 @@ import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitSubModuleParser; import sonia.scm.repository.GitSubModuleParser;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.SubRepository; import sonia.scm.repository.SubRepository;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -104,7 +102,7 @@ public class GitBrowseCommand extends AbstractGitCommand
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BrowserResult getBrowserResult(BrowseCommandRequest request) public BrowserResult getBrowserResult(BrowseCommandRequest request)
throws IOException, NotFoundException { throws IOException {
logger.debug("try to create browse result for {}", request); logger.debug("try to create browse result for {}", request);
BrowserResult result; BrowserResult result;
@@ -166,7 +164,7 @@ public class GitBrowseCommand extends AbstractGitCommand
*/ */
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo, private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
throws IOException, RevisionNotFoundException { throws IOException {
FileObject file = new FileObject(); FileObject file = new FileObject();
@@ -258,7 +256,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return result; return result;
} }
private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException, NotFoundException { private FileObject getEntry(org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId) throws IOException {
RevWalk revWalk = null; RevWalk revWalk = null;
TreeWalk treeWalk = null; TreeWalk treeWalk = null;
@@ -309,7 +307,7 @@ public class GitBrowseCommand extends AbstractGitCommand
return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath()); return Strings.isNullOrEmpty(request.getPath()) || "/".equals(request.getPath());
} }
private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException { private FileObject findChildren(FileObject parent, org.eclipse.jgit.lib.Repository repo, BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
List<FileObject> files = Lists.newArrayList(); List<FileObject> files = Lists.newArrayList();
while (treeWalk.next()) while (treeWalk.next())
{ {
@@ -337,7 +335,7 @@ public class GitBrowseCommand extends AbstractGitCommand
} }
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo, private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException, NotFoundException { BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
String[] pathElements = request.getPath().split("/"); String[] pathElements = request.getPath().split("/");
int currentDepth = 0; int currentDepth = 0;
int limit = pathElements.length; int limit = pathElements.length;
@@ -363,7 +361,7 @@ public class GitBrowseCommand extends AbstractGitCommand
private Map<String, private Map<String,
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo, SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,
ObjectId revision) ObjectId revision)
throws IOException, RevisionNotFoundException { throws IOException {
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
{ {
logger.debug("read submodules of {} at {}", repository.getName(), logger.debug("read submodules of {} at {}", repository.getName(),
@@ -377,7 +375,7 @@ public class GitBrowseCommand extends AbstractGitCommand
PATH_MODULES, baos); PATH_MODULES, baos);
subRepositories = GitSubModuleParser.parse(baos.toString()); subRepositories = GitSubModuleParser.parse(baos.toString());
} }
catch (PathNotFoundException ex) catch (NotFoundException ex)
{ {
logger.trace("could not find .gitmodules", ex); logger.trace("could not find .gitmodules", ex);
subRepositories = Collections.EMPTY_MAP; subRepositories = Collections.EMPTY_MAP;
@@ -388,7 +386,7 @@ public class GitBrowseCommand extends AbstractGitCommand
private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo, private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo,
ObjectId revId, String path) ObjectId revId, String path)
throws IOException, RevisionNotFoundException { throws IOException {
Map<String, SubRepository> subRepositories = subrepositoryCache.get(revId); Map<String, SubRepository> subRepositories = subrepositoryCache.get(revId);
if (subRepositories == null) if (subRepositories == null)

View File

@@ -45,8 +45,6 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import java.io.Closeable; import java.io.Closeable;
@@ -55,6 +53,9 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class GitCatCommand extends AbstractGitCommand implements CatCommand { public class GitCatCommand extends AbstractGitCommand implements CatCommand {
@@ -65,7 +66,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
} }
@Override @Override
public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, PathNotFoundException, RevisionNotFoundException { public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException {
logger.debug("try to read content for {}", request); logger.debug("try to read content for {}", request);
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) { try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) {
closableObjectLoaderContainer.objectLoader.copyTo(output); closableObjectLoaderContainer.objectLoader.copyTo(output);
@@ -73,24 +74,24 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
} }
@Override @Override
public InputStream getCatResultStream(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException { public InputStream getCatResultStream(CatCommandRequest request) throws IOException {
logger.debug("try to read content for {}", request); logger.debug("try to read content for {}", request);
return new InputStreamWrapper(getLoader(request)); return new InputStreamWrapper(getLoader(request));
} }
void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException, PathNotFoundException, RevisionNotFoundException { void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException {
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) { try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) {
closableObjectLoaderContainer.objectLoader.copyTo(output); closableObjectLoaderContainer.objectLoader.copyTo(output);
} }
} }
private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException { private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException {
org.eclipse.jgit.lib.Repository repo = open(); org.eclipse.jgit.lib.Repository repo = open();
ObjectId revId = getCommitOrDefault(repo, request.getRevision()); ObjectId revId = getCommitOrDefault(repo, request.getRevision());
return getLoader(repo, revId, request.getPath()); return getLoader(repo, revId, request.getPath());
} }
private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException, PathNotFoundException, RevisionNotFoundException { private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException {
TreeWalk treeWalk = new TreeWalk(repo); TreeWalk treeWalk = new TreeWalk(repo);
treeWalk.setRecursive(Util.nonNull(path).contains("/")); treeWalk.setRecursive(Util.nonNull(path).contains("/"));
@@ -102,7 +103,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
try { try {
entry = revWalk.parseCommit(revId); entry = revWalk.parseCommit(revId);
} catch (MissingObjectException e) { } catch (MissingObjectException e) {
throw new RevisionNotFoundException(revId.getName()); throw notFound(entity("Revision", revId.getName()).in(repository));
} }
RevTree revTree = entry.getTree(); RevTree revTree = entry.getTree();
@@ -120,7 +121,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk); return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk);
} else { } else {
throw new PathNotFoundException(path); throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository));
} }
} }

View File

@@ -38,6 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -65,10 +66,12 @@ public class GitContext implements Closeable
* *
* *
* @param directory * @param directory
* @param repository
*/ */
public GitContext(File directory) public GitContext(File directory, Repository repository)
{ {
this.directory = directory; this.directory = directory;
this.repository = repository;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -82,8 +85,8 @@ public class GitContext implements Closeable
{ {
logger.trace("close git repository {}", directory); logger.trace("close git repository {}", directory);
GitUtil.close(repository); GitUtil.close(gitRepository);
repository = null; gitRepository = null;
} }
/** /**
@@ -96,21 +99,30 @@ public class GitContext implements Closeable
*/ */
public org.eclipse.jgit.lib.Repository open() throws IOException public org.eclipse.jgit.lib.Repository open() throws IOException
{ {
if (repository == null) if (gitRepository == null)
{ {
logger.trace("open git repository {}", directory); logger.trace("open git repository {}", directory);
repository = GitUtil.open(directory); gitRepository = GitUtil.open(directory);
} }
return gitRepository;
}
Repository getRepository() {
return repository; return repository;
} }
File getDirectory() {
return directory;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private final File directory; private final File directory;
private final Repository repository;
/** Field description */ /** Field description */
private org.eclipse.jgit.lib.Repository repository; private org.eclipse.jgit.lib.Repository gitRepository;
} }

View File

@@ -54,7 +54,6 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter; import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitUtil; import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import java.io.IOException; import java.io.IOException;
@@ -62,6 +61,9 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
/** /**
@@ -86,7 +88,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
* *
* @param context * @param context
* @param repository * @param repository
* @param repositoryDirectory
*/ */
GitLogCommand(GitContext context, sonia.scm.repository.Repository repository) GitLogCommand(GitContext context, sonia.scm.repository.Repository repository)
{ {
@@ -163,7 +164,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
*/ */
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public ChangesetPagingResult getChangesets(LogCommandRequest request) throws RevisionNotFoundException { public ChangesetPagingResult getChangesets(LogCommandRequest request) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("fetch changesets for request: {}", request); logger.debug("fetch changesets for request: {}", request);
} }
@@ -261,11 +262,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
} }
catch (MissingObjectException e) catch (MissingObjectException e)
{ {
throw new RevisionNotFoundException(e.getObjectId().name()); throw notFound(entity("Revision", e.getObjectId().getName()).in(repository));
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new InternalRepositoryException("could not create change log", ex); throw new InternalRepositoryException(repository, "could not create change log", ex);
} }
finally finally
{ {

View File

@@ -0,0 +1,169 @@
package sonia.scm.repository.spi;
import com.google.common.base.Strings;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.user.User;
import java.io.IOException;
import java.text.MessageFormat;
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
"Merge of branch {0} into {1}",
"",
"Automatic merge by SCM-Manager.");
private final GitWorkdirFactory workdirFactory;
GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) {
super(context, repository);
this.workdirFactory = workdirFactory;
}
@Override
public MergeCommandResult merge(MergeCommandRequest request) {
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
Repository repository = workingCopy.get();
logger.debug("cloned repository to folder {}", repository.getWorkTree());
return new MergeWorker(repository, request).merge();
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
}
}
@Override
public MergeDryRunCommandResult dryRun(MergeCommandRequest request) {
try {
Repository repository = context.open();
ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
return new MergeDryRunCommandResult(merger.merge(repository.resolve(request.getBranchToMerge()), repository.resolve(request.getTargetBranch())));
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
}
}
private class MergeWorker {
private final String target;
private final String toMerge;
private final Person author;
private final Git clone;
private final String messageTemplate;
private MergeWorker(Repository clone, MergeCommandRequest request) {
this.target = request.getTargetBranch();
this.toMerge = request.getBranchToMerge();
this.author = request.getAuthor();
this.messageTemplate = request.getMessageTemplate();
this.clone = new Git(clone);
}
private MergeCommandResult merge() throws IOException {
checkOutTargetBranch();
MergeResult result = doMergeInClone();
if (result.getMergeStatus().isSuccessful()) {
doCommit();
push();
return MergeCommandResult.success();
} else {
return analyseFailure(result);
}
}
private void checkOutTargetBranch() {
try {
clone.checkout().setName(target).call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e);
}
}
private MergeResult doMergeInClone() throws IOException {
MergeResult result;
try {
result = clone.merge()
.setCommit(false) // we want to set the author manually
.include(toMerge, resolveRevision(toMerge))
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e);
}
return result;
}
private void doCommit() {
logger.debug("merged branch {} into {}", toMerge, target);
Person authorToUse = determineAuthor();
try {
clone.commit()
.setAuthor(authorToUse.getName(), authorToUse.getMail())
.setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e);
}
}
private String determineMessageTemplate() {
if (Strings.isNullOrEmpty(messageTemplate)) {
return MERGE_COMMIT_MESSAGE_TEMPLATE;
} else {
return messageTemplate;
}
}
private Person determineAuthor() {
if (author == null) {
Subject subject = SecurityUtils.getSubject();
User user = subject.getPrincipals().oneByType(User.class);
String name = user.getDisplayName();
String email = user.getMail();
logger.debug("no author set; using logged in user: {} <{}>", name, email);
return new Person(name, email);
} else {
return author;
}
}
private void push() {
try {
clone.push().call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not push merged branch " + toMerge + " to origin", e);
}
logger.debug("pushed merged branch {}", target);
}
private MergeCommandResult analyseFailure(MergeResult result) {
logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
return MergeCommandResult.failure(result.getConflicts().keySet());
}
private ObjectId resolveRevision(String branchToMerge) throws IOException {
ObjectId resolved = clone.getRepository().resolve(branchToMerge);
if (resolved == null) {
return clone.getRepository().resolve("origin/" + branchToMerge);
} else {
return resolved;
}
}
}
}

View File

@@ -17,6 +17,8 @@ import java.io.IOException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.List; import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Slf4j @Slf4j
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand { public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
@@ -26,7 +28,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
} }
private Modifications createModifications(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk, String revision) private Modifications createModifications(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk, String revision)
throws IOException, UnsupportedModificationTypeException { throws IOException {
treeWalk.reset(); treeWalk.reset();
treeWalk.setRecursive(true); treeWalk.setRecursive(true);
if (commit.getParentCount() > 0) { if (commit.getParentCount() > 0) {
@@ -73,12 +75,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
} }
} catch (IOException ex) { } catch (IOException ex) {
log.error("could not open repository", ex); log.error("could not open repository", ex);
throw new InternalRepositoryException(ex); throw new InternalRepositoryException(entity(repository), "could not open repository", ex);
} catch (UnsupportedModificationTypeException ex) {
log.error("Unsupported modification type", ex);
throw new InternalRepositoryException(ex);
} finally { } finally {
GitUtil.release(revWalk); GitUtil.release(revWalk);
GitUtil.close(gitRepository); GitUtil.close(gitRepository);
@@ -100,7 +97,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
} else if (type == DiffEntry.ChangeType.DELETE) { } else if (type == DiffEntry.ChangeType.DELETE) {
modifications.getRemoved().add(entry.getOldPath()); modifications.getRemoved().add(entry.getOldPath());
} else { } else {
throw new UnsupportedModificationTypeException(MessageFormat.format("The modification type: {0} is not supported.", type)); throw new UnsupportedModificationTypeException(entity(repository), MessageFormat.format("The modification type: {0} is not supported.", type));
} }
} }
} }

View File

@@ -77,7 +77,6 @@ public class GitOutgoingCommand extends AbstractGitIncomingOutgoingCommand
* @return * @return
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Override @Override
public ChangesetPagingResult getOutgoingChangesets( public ChangesetPagingResult getOutgoingChangesets(

View File

@@ -101,7 +101,6 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
* @return * @return
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Override @Override
public PullResponse pull(PullCommandRequest request) public PullResponse pull(PullCommandRequest request)
@@ -249,7 +248,7 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand
} }
catch (GitAPIException ex) catch (GitAPIException ex)
{ {
throw new InternalRepositoryException("error durring pull", ex); throw new InternalRepositoryException(repository, "error during pull", ex);
} }
return response; return response;

View File

@@ -85,7 +85,6 @@ public class GitPushCommand extends AbstractGitPushOrPullCommand
* @return * @return
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Override @Override
public PushResponse push(PushCommandRequest request) public PushResponse push(PushCommandRequest request)

View File

@@ -63,7 +63,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.INCOMING, Command.INCOMING,
Command.OUTGOING, Command.OUTGOING,
Command.PUSH, Command.PUSH,
Command.PULL Command.PULL,
Command.MERGE
); );
//J+ //J+
@@ -72,7 +73,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) { public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) {
this.handler = handler; this.handler = handler;
this.repository = repository; this.repository = repository;
this.context = new GitContext(handler.getDirectory(repository)); this.context = new GitContext(handler.getDirectory(repository), repository);
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -240,6 +241,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
return new GitTagsCommand(context, repository); return new GitTagsCommand(context, repository);
} }
@Override
public MergeCommand getMergeCommand() {
return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */

View File

@@ -95,7 +95,7 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand
} }
catch (GitAPIException ex) catch (GitAPIException ex)
{ {
throw new InternalRepositoryException("could not read tags from repository", ex); throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
} }
finally finally
{ {

View File

@@ -0,0 +1,62 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
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 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 SimpleGitWorkdirFactory() {
this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
}
public SimpleGitWorkdirFactory(File poolDirectory) {
this.poolDirectory = poolDirectory;
poolDirectory.mkdirs();
}
public WorkingCopy createWorkingCopy(GitContext gitContext) {
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(bareRepository.getAbsolutePath())
.setDirectory(target)
.call()
.getRepository();
}
private void close(Repository repository) {
repository.close();
try {
FileUtils.delete(repository.getWorkTree(), FileUtils.RECURSIVE);
} catch (IOException e) {
logger.warn("could not delete temporary git workdir '{}'", repository.getWorkTree(), e);
}
}
}

View File

@@ -1,9 +1,10 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
public class UnsupportedModificationTypeException extends InternalRepositoryException { public class UnsupportedModificationTypeException extends InternalRepositoryException {
public UnsupportedModificationTypeException(String message) { public UnsupportedModificationTypeException(ContextEntry.ContextBuilder entity, String message) {
super(message); super(entity, message);
} }
} }

View File

@@ -0,0 +1,12 @@
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

@@ -151,7 +151,6 @@ public class GitRepositoryViewer
* @return * @return
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
private BranchesModel createBranchesModel(Repository repository) private BranchesModel createBranchesModel(Repository repository)
throws IOException throws IOException

View File

@@ -41,6 +41,8 @@ import org.mapstruct.factory.Mappers;
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper; import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper; import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory;
/** /**
@@ -63,5 +65,7 @@ public class GitServletModule extends ServletModule
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass()); bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass()); bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
} }
} }

View File

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

View File

@@ -61,6 +61,9 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock @Mock
private ConfigurationStoreFactory factory; private ConfigurationStoreFactory factory;
@Mock
private GitWorkdirFactory gitWorkdirFactory;
@Override @Override
protected void checkDirectory(File directory) { protected void checkDirectory(File directory) {
File head = new File(directory, "HEAD"); File head = new File(directory, "HEAD");
@@ -84,7 +87,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
File directory) { File directory) {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
new DefaultFileSystem(), scheduler); new DefaultFileSystem(), scheduler, gitWorkdirFactory);
repositoryHandler.init(contextProvider); repositoryHandler.init(contextProvider);
@@ -100,7 +103,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test @Test
public void getDirectory() { public void getDirectory() {
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
new DefaultFileSystem(), scheduler); new DefaultFileSystem(), scheduler, gitWorkdirFactory);
GitConfig gitConfig = new GitConfig(); GitConfig gitConfig = new GitConfig();
gitConfig.setRepositoryDirectory(new File("/path")); gitConfig.setRepositoryDirectory(new File("/path"));

View File

@@ -50,8 +50,10 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
@After @After
public void close() public void close()
{ {
if (context != null) {
context.close(); context.close();
} }
}
/** /**
* Method description * Method description
@@ -63,7 +65,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
{ {
if (context == null) if (context == null)
{ {
context = new GitContext(repositoryDirectory); context = new GitContext(repositoryDirectory, repository);
} }
return context; return context;

View File

@@ -85,7 +85,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
* *
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetBlameResult() throws IOException public void testGetBlameResult() throws IOException
@@ -119,7 +118,6 @@ public class GitBlameCommandTest extends AbstractGitCommandTestBase
* *
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetBlameResultWithRevision() public void testGetBlameResultWithRevision()

View File

@@ -32,7 +32,6 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.junit.Test; import org.junit.Test;
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult; import sonia.scm.repository.BrowserResult;
import sonia.scm.repository.FileObject; import sonia.scm.repository.FileObject;
import sonia.scm.repository.GitConstants; import sonia.scm.repository.GitConstants;
@@ -54,7 +53,7 @@ import static org.junit.Assert.assertTrue;
public class GitBrowseCommandTest extends AbstractGitCommandTestBase { public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
@Test @Test
public void testGetFile() throws IOException, NotFoundException { public void testDefaultBranch() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest(); BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("a.txt"); request.setPath("a.txt");
BrowserResult result = createCommand().getBrowserResult(request); BrowserResult result = createCommand().getBrowserResult(request);
@@ -63,7 +62,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testDefaultDefaultBranch() throws IOException, NotFoundException { public void testDefaultDefaultBranch() throws IOException {
// without default branch, the repository head should be used // without default branch, the repository head should be used
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(root); assertNotNull(root);
@@ -78,7 +77,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testExplicitDefaultBranch() throws IOException, NotFoundException { public void testExplicitDefaultBranch() throws IOException {
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch"); repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
@@ -91,7 +90,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testBrowse() throws IOException, NotFoundException { public void testBrowse() throws IOException {
FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile(); FileObject root = createCommand().getBrowserResult(new BrowseCommandRequest()).getFile();
assertNotNull(root); assertNotNull(root);
@@ -113,7 +112,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testBrowseSubDirectory() throws IOException, NotFoundException { public void testBrowseSubDirectory() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest(); BrowseCommandRequest request = new BrowseCommandRequest();
request.setPath("c"); request.setPath("c");
@@ -143,7 +142,7 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testRecursive() throws IOException, NotFoundException { public void testRecusive() throws IOException {
BrowseCommandRequest request = new BrowseCommandRequest(); BrowseCommandRequest request = new BrowseCommandRequest();
request.setRecursive(true); request.setRecursive(true);

View File

@@ -32,10 +32,13 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.GitConstants; import sonia.scm.repository.GitConstants;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -52,8 +55,11 @@ import static org.junit.Assert.assertEquals;
*/ */
public class GitCatCommandTest extends AbstractGitCommandTestBase { public class GitCatCommandTest extends AbstractGitCommandTestBase {
@Rule
public final ExpectedException expectedException = ExpectedException.none();
@Test @Test
public void testDefaultBranch() throws IOException, PathNotFoundException, RevisionNotFoundException { public void testDefaultBranch() throws IOException {
// without default branch, the repository head should be used // without default branch, the repository head should be used
CatCommandRequest request = new CatCommandRequest(); CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt"); request.setPath("a.txt");
@@ -66,7 +72,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testCat() throws IOException, PathNotFoundException, RevisionNotFoundException { public void testCat() throws IOException {
CatCommandRequest request = new CatCommandRequest(); CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt"); request.setPath("a.txt");
@@ -75,32 +81,58 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
} }
@Test @Test
public void testSimpleCat() throws IOException, PathNotFoundException, RevisionNotFoundException { public void testSimpleCat() throws IOException {
CatCommandRequest request = new CatCommandRequest(); CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt"); request.setPath("b.txt");
assertEquals("b", execute(request)); assertEquals("b", execute(request));
} }
@Test(expected = PathNotFoundException.class) @Test
public void testUnknownFile() throws IOException, PathNotFoundException, RevisionNotFoundException { public void testUnknownFile() throws IOException {
CatCommandRequest request = new CatCommandRequest(); CatCommandRequest request = new CatCommandRequest();
request.setPath("unknown"); request.setPath("unknown");
execute(request);
expectedException.expect(new BaseMatcher<Object>() {
@Override
public void describeTo(Description description) {
description.appendText("expected NotFoundException for path");
} }
@Test(expected = RevisionNotFoundException.class) @Override
public void testUnknownRevision() throws IOException, PathNotFoundException, RevisionNotFoundException { public boolean matches(Object item) {
CatCommandRequest request = new CatCommandRequest(); return "Path".equals(((NotFoundException)item).getContext().get(0).getType());
}
});
request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
request.setPath("a.txt");
execute(request); execute(request);
} }
@Test @Test
public void testSimpleStream() throws IOException, PathNotFoundException, RevisionNotFoundException { public void testUnknownRevision() throws IOException {
CatCommandRequest request = new CatCommandRequest();
request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
request.setPath("a.txt");
expectedException.expect(new BaseMatcher<Object>() {
@Override
public void describeTo(Description description) {
description.appendText("expected NotFoundException for revision");
}
@Override
public boolean matches(Object item) {
return "Revision".equals(((NotFoundException)item).getContext().get(0).getType());
}
});
execute(request);
}
@Test
public void testSimpleStream() throws IOException {
CatCommandRequest request = new CatCommandRequest(); CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt"); request.setPath("b.txt");
@@ -113,7 +145,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
catResultStream.close(); catResultStream.close();
} }
private String execute(CatCommandRequest request) throws IOException, PathNotFoundException, RevisionNotFoundException { private String execute(CatCommandRequest request) throws IOException {
String content = null; String content = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();

View File

@@ -61,7 +61,6 @@ public class GitIncomingCommandTest
* *
* @throws GitAPIException * @throws GitAPIException
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetIncomingChangesets() public void testGetIncomingChangesets()
@@ -95,7 +94,6 @@ public class GitIncomingCommandTest
* *
* @throws GitAPIException * @throws GitAPIException
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetIncomingChangesetsWithAllreadyPullChangesets() public void testGetIncomingChangesetsWithAllreadyPullChangesets()
@@ -105,7 +103,7 @@ public class GitIncomingCommandTest
commit(outgoing, "added a"); commit(outgoing, "added a");
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomingRepository); GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, null), incomingRepository);
PullCommandRequest req = new PullCommandRequest(); PullCommandRequest req = new PullCommandRequest();
req.setRemoteRepository(outgoingRepository); req.setRemoteRepository(outgoingRepository);
pull.pull(req); pull.pull(req);
@@ -132,7 +130,6 @@ public class GitIncomingCommandTest
* *
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetIncomingChangesetsWithEmptyRepository() public void testGetIncomingChangesetsWithEmptyRepository()
@@ -156,7 +153,6 @@ public class GitIncomingCommandTest
* *
* @throws GitAPIException * @throws GitAPIException
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
@Ignore @Ignore
@@ -191,7 +187,7 @@ public class GitIncomingCommandTest
*/ */
private GitIncomingCommand createCommand() private GitIncomingCommand createCommand()
{ {
return new GitIncomingCommand(handler, new GitContext(incomingDirectory), return new GitIncomingCommand(handler, new GitContext(incomingDirectory, null),
incomingRepository); incomingRepository);
} }
} }

View File

@@ -0,0 +1,139 @@
package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.user.User;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
public class GitMergeCommandTest extends AbstractGitCommandTestBase {
private static final String REALM = "AdminRealm";
@Rule
public ShiroRule shiro = new ShiroRule();
@Test
public void shouldDetectMergeableBranches() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("mergeable");
request.setTargetBranch("master");
boolean mergeable = command.dryRun(request).isMergeable();
assertThat(mergeable).isTrue();
}
@Test
public void shouldDetectNotMergeableBranches() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("test-branch");
request.setTargetBranch("master");
boolean mergeable = command.dryRun(request).isMergeable();
assertThat(mergeable).isFalse();
}
@Test
public void shouldMergeMergeableBranches() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
assertThat(message).contains("master", "mergeable");
// We expect the merge result of file b.txt here by looking up the sha hash of its content.
// If the file is missing (aka not merged correctly) this will throw a MissingObjectException:
byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes();
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
}
@Test
public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setMessageTemplate("simple");
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
String message = mergeCommit.getFullMessage();
assertThat(message).isEqualTo("simple");
}
@Test
public void shouldNotMergeConflictingBranches() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("test-branch");
request.setTargetBranch("master");
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isFalse();
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
}
@Test
@SubjectAware(username = "admin", password = "secret")
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
shiro.setSubject(
new Subject.Builder()
.principals(new SimplePrincipalCollection(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM))
.buildSubject());
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> mergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
PersonIdent mergeAuthor = mergeCommit.iterator().next().getAuthorIdent();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
}
private GitMergeCommand createCommand() {
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
}
}

View File

@@ -18,8 +18,8 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
@Before @Before
public void init() { public void init() {
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory), incomingRepository); incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, null), incomingRepository);
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory), outgoingRepository); outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, null), outgoingRepository);
} }
@Test @Test
@@ -63,12 +63,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
} }
void pushOutgoingAndPullIncoming() throws IOException { void pushOutgoingAndPullIncoming() throws IOException {
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory), GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository); outgoingRepository);
PushCommandRequest request = new PushCommandRequest(); PushCommandRequest request = new PushCommandRequest();
request.setRemoteRepository(incomingRepository); request.setRemoteRepository(incomingRepository);
cmd.push(request); cmd.push(request);
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory), GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, null),
incomingRepository); incomingRepository);
PullCommandRequest pullRequest = new PullCommandRequest(); PullCommandRequest pullRequest = new PullCommandRequest();
pullRequest.setRemoteRepository(incomingRepository); pullRequest.setRemoteRepository(incomingRepository);

View File

@@ -61,7 +61,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
* *
* @throws GitAPIException * @throws GitAPIException
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetOutgoingChangesets() public void testGetOutgoingChangesets()
@@ -95,7 +94,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
* *
* @throws GitAPIException * @throws GitAPIException
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetOutgoingChangesetsWithAlreadyPushedChanges() public void testGetOutgoingChangesetsWithAlreadyPushedChanges()
@@ -106,7 +104,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
commit(outgoing, "added a"); commit(outgoing, "added a");
GitPushCommand push = new GitPushCommand(handler, GitPushCommand push = new GitPushCommand(handler,
new GitContext(outgoingDirectory), new GitContext(outgoingDirectory, null),
outgoingRepository); outgoingRepository);
PushCommandRequest req = new PushCommandRequest(); PushCommandRequest req = new PushCommandRequest();
@@ -135,7 +133,6 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
* *
* *
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testGetOutgoingChangesetsWithEmptyRepository() public void testGetOutgoingChangesetsWithEmptyRepository()
@@ -161,7 +158,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
*/ */
private GitOutgoingCommand createCommand() private GitOutgoingCommand createCommand()
{ {
return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory), return new GitOutgoingCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository); outgoingRepository);
} }
} }

View File

@@ -61,7 +61,6 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
* *
* @throws GitAPIException * @throws GitAPIException
* @throws IOException * @throws IOException
* @throws RepositoryException
*/ */
@Test @Test
public void testPush() public void testPush()
@@ -99,7 +98,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
*/ */
private GitPushCommand createCommand() private GitPushCommand createCommand()
{ {
return new GitPushCommand(handler, new GitContext(outgoingDirectory), return new GitPushCommand(handler, new GitContext(outgoingDirectory, null),
outgoingRepository); outgoingRepository);
} }
} }

View File

@@ -0,0 +1,87 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File masterRepo = createRepositoryDirectory();
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
assertThat(workingCopy.get().getDirectory())
.exists()
.isNotEqualTo(masterRepo)
.isDirectory();
assertThat(new File(workingCopy.get().getWorkTree(), "a.txt"))
.exists()
.isFile()
.hasContent("a\nline for blame");
}
}
@Test
public void cloneFromPoolShouldBeClosed() throws IOException {
PoolWithSpy factory = new PoolWithSpy(temporaryFolder.newFolder());
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
assertThat(workingCopy).isNotNull();
}
verify(factory.createdClone).close();
}
@Test
public void cloneFromPoolShouldNotBeReused() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File firstDirectory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
firstDirectory = workingCopy.get().getDirectory();
}
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
File secondDirectory = workingCopy.get().getDirectory();
assertThat(secondDirectory).isNotEqualTo(firstDirectory);
}
}
@Test
public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
File directory;
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
directory = workingCopy.get().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

@@ -59,6 +59,10 @@
<extensions>true</extensions> <extensions>true</extensions>
<configuration> <configuration>
<corePlugin>true</corePlugin> <corePlugin>true</corePlugin>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -1,348 +0,0 @@
/**
* 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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
import sonia.scm.SCMContext;
import sonia.scm.installer.HgInstallerFactory;
import sonia.scm.installer.HgPackage;
import sonia.scm.installer.HgPackageReader;
import sonia.scm.installer.HgPackages;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgRepositoryHandler;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.List;
import javax.ws.rs.Consumes;
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.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("config/repositories/hg")
public class HgConfigResource
{
/**
* Constructs ...
*
*
*
*
* @param client
* @param handler
* @param pkgReader
*/
@Inject
public HgConfigResource(AdvancedHttpClient client,
HgRepositoryHandler handler, HgPackageReader pkgReader)
{
this.client = client;
this.handler = handler;
this.pkgReader = pkgReader;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
*
* @return
*/
@POST
@Path("auto-configuration")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgConfig autoConfiguration(@Context UriInfo uriInfo)
{
return autoConfiguration(uriInfo, null);
}
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*/
@POST
@Path("auto-configuration")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgConfig autoConfiguration(@Context UriInfo uriInfo, HgConfig config)
{
if (config == null)
{
config = new HgConfig();
}
handler.doAutoConfiguration(config);
return handler.getConfig();
}
/**
* Method description
*
*
*
* @param id
* @return
*/
@POST
@Path("packages/{pkgId}")
public Response installPackage(@PathParam("pkgId") String id)
{
Response response = null;
HgPackage pkg = pkgReader.getPackage(id);
if (pkg != null)
{
if (HgInstallerFactory.createInstaller().installPackage(client, handler,
SCMContext.getContext().getBaseDirectory(), pkg))
{
response = Response.noContent().build();
}
else
{
response =
Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
else
{
response = Response.status(Response.Status.NOT_FOUND).build();
}
return response;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgConfig getConfig()
{
HgConfig config = handler.getConfig();
if (config == null)
{
config = new HgConfig();
}
return config;
}
/**
* Method description
*
*
* @return
*/
@GET
@Path("installations/hg")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public InstallationsResponse getHgInstallations()
{
List<String> installations =
HgInstallerFactory.createInstaller().getHgInstallations();
return new InstallationsResponse(installations);
}
/**
* Method description
*
*
* @return
*/
@GET
@Path("packages")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public HgPackages getPackages()
{
return pkgReader.getPackages();
}
/**
* Method description
*
*
* @return
*/
@GET
@Path("installations/python")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public InstallationsResponse getPythonInstallations()
{
List<String> installations =
HgInstallerFactory.createInstaller().getPythonInstallations();
return new InstallationsResponse(installations);
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param uriInfo
* @param config
*
* @return
*
* @throws IOException
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response setConfig(@Context UriInfo uriInfo, HgConfig config)
throws IOException
{
handler.setConfig(config);
handler.storeConfig();
return Response.created(uriInfo.getRequestUri()).build();
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 11/04/25
* @author Enter your name here...
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "installations")
public static class InstallationsResponse
{
/**
* Constructs ...
*
*/
public InstallationsResponse() {}
/**
* Constructs ...
*
*
* @param paths
*/
public InstallationsResponse(List<String> paths)
{
this.paths = paths;
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public List<String> getPaths()
{
return paths;
}
//~--- set methods --------------------------------------------------------
/**
* Method description
*
*
* @param paths
*/
public void setPaths(List<String> paths)
{
this.paths = paths;
}
//~--- fields -------------------------------------------------------------
/** Field description */
@XmlElement(name = "path")
private List<String> paths;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private AdvancedHttpClient client;
/** Field description */
private HgRepositoryHandler handler;
/** Field description */
private HgPackageReader pkgReader;
}

View File

@@ -272,7 +272,7 @@ public class AbstractHgHandler
} catch (JAXBException ex) { } catch (JAXBException ex) {
logger.error("could not parse result", ex); logger.error("could not parse result", ex);
throw new InternalRepositoryException("could not parse result", ex); throw new InternalRepositoryException(repository, "could not parse result", ex);
} }
} }

View File

@@ -36,6 +36,9 @@ package sonia.scm.repository.spi;
import com.aragost.javahg.commands.ExecutionException; import com.aragost.javahg.commands.ExecutionException;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables; import com.google.common.io.Closeables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.web.HgUtil; import sonia.scm.web.HgUtil;
@@ -46,6 +49,8 @@ import java.io.OutputStream;
public class HgCatCommand extends AbstractCommand implements CatCommand { public class HgCatCommand extends AbstractCommand implements CatCommand {
private static final Logger log = LoggerFactory.getLogger(HgCatCommand.class);
HgCatCommand(HgCommandContext context, Repository repository) { HgCatCommand(HgCommandContext context, Repository repository) {
super(context, repository); super(context, repository);
} }
@@ -70,7 +75,8 @@ public class HgCatCommand extends AbstractCommand implements CatCommand {
try { try {
return cmd.execute(request.getPath()); return cmd.execute(request.getPath());
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new InternalRepositoryException(e); log.error("could not execute cat command", e);
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity(getRepository()), "could not execute cat command", e);
} }
} }
} }

View File

@@ -103,7 +103,7 @@ public class HgIncomingCommand extends AbstractCommand
} }
else else
{ {
throw new InternalRepositoryException("could not execute incoming command", ex); throw new InternalRepositoryException(getRepository(), "could not execute incoming command", ex);
} }
} }

View File

@@ -103,7 +103,7 @@ public class HgOutgoingCommand extends AbstractCommand
} }
else else
{ {
throw new InternalRepositoryException("could not execute outgoing command", ex); throw new InternalRepositoryException(getRepository(), "could not execute outgoing command", ex);
} }
} }

Some files were not shown because too many files have changed in this diff Show More