This commit is contained in:
Mohamed Karray
2018-11-15 11:28:33 +01:00
381 changed files with 12331 additions and 11177 deletions

View File

@@ -485,6 +485,7 @@
see https://blogs.oracle.com/darcy/entry/bootclasspath_older_source
-->
<compilerArgument>-Xlint:unchecked,-options</compilerArgument>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
@@ -740,7 +741,7 @@
<properties>
<!-- test libraries -->
<mockito.version>2.10.0</mockito.version>
<mockito.version>2.23.0</mockito.version>
<hamcrest.version>1.3</hamcrest.version>
<junit.version>5.2.0</junit.version>
@@ -757,7 +758,7 @@
<guice.version>4.0</guice.version>
<!-- event bus -->
<legman.version>1.3.0</legman.version>
<legman.version>1.4.2</legman.version>
<!-- webserver -->
<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;
public class AlreadyExistsException extends Exception {
import java.util.List;
public AlreadyExistsException(String message) {
super(message);
import static java.util.Collections.singletonList;
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;
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.
*/
T create(T object) throws AlreadyExistsException;
T create(T object);
/**
* Removes a persistent object.
*
*
* @param object to delete
*
* @throws IOException
*/
void delete(T object) throws NotFoundException;
void delete(T object);
/**
* Modifies a persistent object.
*
*
* @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
*/
void refresh(T object) throws NotFoundException;
void refresh(T object);
//~--- get methods ----------------------------------------------------------

View File

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

View File

@@ -1,10 +1,38 @@
package sonia.scm;
public class NotFoundException extends RuntimeException {
public NotFoundException(String type, String id) {
super(type + " with id '" + id + "' not found");
import java.util.Collections;
import java.util.List;
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;
import java.util.Collections;
/**
*
* @author Sebastian Sdorra
* @version 1.6
*/
public class NotSupportedFeatuerException extends Exception
{
public class NotSupportedFeatureException extends ExceptionWithContext {
/** Field description */
private static final long serialVersionUID = 256498734456613496L;
//~--- constructors ---------------------------------------------------------
private static final String CODE = "9SR8G0kmU1";
/**
* Constructs ...
*
*/
public NotSupportedFeatuerException() {}
/**
* Constructs ...
*
*
* @param message
*/
public NotSupportedFeatuerException(String message)
public NotSupportedFeatureException(String feature)
{
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 */
public static final String PATTERN_ALL = "/*";
/** Field description */
public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
/** Field description */
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 */
public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
/** Field description */
public static final String PATTERN_USERS = REST_API_PATH + "/users*";
/** authentication priority */
public static final int PRIORITY_AUTHENTICATION = 5000;

View File

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

View File

@@ -40,6 +40,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.ConfigurationException;
import sonia.scm.ContextEntry;
import sonia.scm.io.CommandResult;
import sonia.scm.io.ExtendedCommand;
import sonia.scm.io.FileSystem;
@@ -80,13 +81,13 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
}
@Override
public Repository create(Repository repository) throws AlreadyExistsException {
public Repository create(Repository repository) {
File directory = repositoryLocationResolver.getInitialNativeDirectory(repository);
if (directory != null && directory.exists()) {
throw new AlreadyExistsException();
throw new AlreadyExistsException(repository);
}
checkPath(directory);
checkPath(directory, repository);
try {
fileSystem.create(directory);
@@ -120,15 +121,20 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
@Override
public void delete(Repository repository) {
File directory = null;
try {
directory = repositoryLocationResolver.getRepositoryDirectory(repository);
} catch (IOException e) {
throw new InternalRepositoryException(repository, "Cannot get the repository directory");
}
try {
File directory = repositoryLocationResolver.getRepositoryDirectory(repository);
if (directory.exists()) {
fileSystem.destroy(directory);
} else {
logger.warn("repository {} not found", repository.getNamespaceAndName());
}
} 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);
}
}
@@ -178,7 +184,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
}
protected void create(Repository repository, File directory)
throws IOException, AlreadyExistsException {
throws IOException {
ExtendedCommand cmd = buildCreateCommand(repository, directory);
CommandResult result = cmd.execute();
@@ -233,9 +239,9 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
* Check path for existing repositories
*
* @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) {
if (directory == null) {
return;
}
@@ -246,9 +252,7 @@ public abstract class AbstractSimpleRepositoryHandler<C extends RepositoryConfig
logger.trace("check {} for existing repository", parent);
if (isRepository(parent)) {
logger.error("parent path {} is a repository", parent);
throw new AlreadyExistsException();
throw new InternalRepositoryException(repository, "parent path" + parent + " is a repository");
}
parent = parent.getParentFile();

View File

@@ -1,15 +1,34 @@
package sonia.scm.repository;
public class InternalRepositoryException extends RuntimeException {
public InternalRepositoryException(Throwable ex) {
super(ex);
import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
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) {
super(msg, ex);
public InternalRepositoryException(ContextEntry.ContextBuilder context, String message, Exception cause) {
this(context.build(), message, cause);
}
public InternalRepositoryException(String message) {
super(message);
public InternalRepositoryException(Repository repository, String 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;
}
public String logString() {
return getNamespace() + "/" + getName();
}
@Override
public String toString() {
return getNamespace() + "/" + getName();
return logString();
}
@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.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
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.XmlTransient;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Source code repository.
@@ -79,7 +81,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified;
private String namespace;
private String name;
private List<Permission> permissions;
private final Set<Permission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
@@ -127,7 +129,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name;
this.contact = contact;
this.description = description;
this.permissions = Lists.newArrayList();
if (Util.isNotEmpty(permissions)) {
this.permissions.addAll(Arrays.asList(permissions));
@@ -200,12 +201,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return new NamespaceAndName(getNamespace(), getName());
}
public List<Permission> getPermissions() {
if (permissions == null) {
permissions = Lists.newArrayList();
}
return permissions;
public Collection<Permission> getPermissions() {
return Collections.unmodifiableCollection(permissions);
}
/**
@@ -300,8 +297,17 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
public void setPermissions(Collection<Permission> 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) {

View File

@@ -36,7 +36,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.Handler;
import sonia.scm.NotSupportedFeatuerException;
import sonia.scm.NotSupportedFeatureException;
import sonia.scm.plugin.ExtensionPoint;
/**
@@ -70,9 +70,9 @@ public interface RepositoryHandler
* @return {@link ImportHandler} for the repository type of this handler
* @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.

View File

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

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.AlreadyExistsException;
import sonia.scm.ManagerDecorator;
import sonia.scm.Type;
@@ -82,7 +81,7 @@ public class RepositoryManagerDecorator
* {@inheritDoc}
*/
@Override
public void importRepository(Repository repository) throws IOException, AlreadyExistsException {
public void importRepository(Repository repository) throws IOException {
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.repository.BrowserResult;
@@ -46,7 +45,6 @@ import sonia.scm.repository.FileObject;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.BrowseCommand;
import sonia.scm.repository.spi.BrowseCommandRequest;
@@ -136,7 +134,7 @@ public final class BrowseCommandBuilder
*
* @throws IOException
*/
public BrowserResult getBrowserResult() throws IOException, NotFoundException {
public BrowserResult getBrowserResult() throws IOException {
BrowserResult result = null;
if (disableCache)

View File

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

View File

@@ -66,6 +66,10 @@ public enum Command
/**
* @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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.DiffCommand;
import sonia.scm.repository.spi.DiffCommandRequest;
import sonia.scm.util.IOUtil;
@@ -104,7 +103,7 @@ public final class DiffCommandBuilder
*
* @throws IOException
*/
public DiffCommandBuilder retriveContent(OutputStream outputStream) throws IOException, RevisionNotFoundException {
public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException {
getDiffResult(outputStream);
return this;
@@ -119,7 +118,7 @@ public final class DiffCommandBuilder
*
* @throws IOException
*/
public String getContent() throws IOException, RevisionNotFoundException {
public String getContent() throws IOException {
String content = null;
ByteArrayOutputStream baos = null;
@@ -199,7 +198,7 @@ public final class DiffCommandBuilder
*
* @throws IOException
*/
private void getDiffResult(OutputStream outputStream) throws IOException, RevisionNotFoundException {
private void getDiffResult(OutputStream outputStream) throws IOException {
Preconditions.checkNotNull(outputStream, "OutputStream is required");
Preconditions.checkArgument(request.isValid(),
"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.Repository;
import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.LogCommand;
import sonia.scm.repository.spi.LogCommandRequest;
@@ -165,7 +164,7 @@ public final class LogCommandBuilder
*
* @throws IOException
*/
public Changeset getChangeset(String id) throws IOException, RevisionNotFoundException {
public Changeset getChangeset(String id) throws IOException {
Changeset changeset;
if (disableCache)
@@ -224,7 +223,7 @@ public final class LogCommandBuilder
*
* @throws IOException
*/
public ChangesetPagingResult getChangesets() throws IOException, RevisionNotFoundException {
public ChangesetPagingResult getChangesets() throws IOException {
ChangesetPagingResult cpr;
if (disableCache)
@@ -398,6 +397,11 @@ public final class LogCommandBuilder
return this;
}
public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) {
request.setAncestorChangeset(ancestorChangeset);
return this;
}
//~--- inner classes --------------------------------------------------------
/**

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.Repository;
import sonia.scm.repository.RepositoryCacheKey;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.spi.ModificationsCommand;
import sonia.scm.repository.spi.ModificationsCommandRequest;
@@ -67,7 +66,7 @@ public final class ModificationsCommandBuilder {
return this;
}
public Modifications getModifications() throws IOException, RevisionNotFoundException {
public Modifications getModifications() throws IOException {
Modifications modifications;
if (disableCache) {
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.BundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17
*/
@Slf4j
@@ -353,6 +354,22 @@ public final class RepositoryService implements Closeable {
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.
*

View File

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

View File

@@ -35,7 +35,6 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.NotFoundException;
import sonia.scm.repository.BrowserResult;
import java.io.IOException;
@@ -60,5 +59,5 @@ public interface BrowseCommand
*
* @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;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -47,7 +44,7 @@ import java.io.OutputStream;
*/
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;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
@@ -56,5 +54,5 @@ public interface DiffCommand
* @throws IOException
* @throws RuntimeException
*/
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException, RevisionNotFoundException;
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException;
}

View File

@@ -109,7 +109,10 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
this.format = format;
}
//~--- get methods ----------------------------------------------------------
public void setAncestorChangeset(String ancestorChangeset) {
this.ancestorChangeset = ancestorChangeset;
}
//~--- get methods ----------------------------------------------------------
/**
* Return the output format of the diff command.
@@ -124,8 +127,13 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
return format;
}
//~--- fields ---------------------------------------------------------------
public String getAncestorChangeset() {
return ancestorChangeset;
}
//~--- fields ---------------------------------------------------------------
/** diff format */
private DiffFormat format = DiffFormat.NATIVE;
private String ancestorChangeset;
}

View File

@@ -40,10 +40,12 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
/**
*
* @author Sebastian Sdorra
@@ -71,18 +73,18 @@ public final class HookEventFacade
//~--- methods --------------------------------------------------------------
public HookEventHandler handle(String id) throws RepositoryNotFoundException {
public HookEventHandler handle(String 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));
}
public HookEventHandler handle(Repository repository) throws RepositoryNotFoundException {
public HookEventHandler handle(Repository repository) {
if (repository == null)
{
throw new RepositoryNotFoundException(repository);
throw notFound(entity(repository));
}
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.ChangesetPagingResult;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
@@ -50,7 +49,7 @@ import java.io.IOException;
*/
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

@@ -84,7 +84,8 @@ public final class LogCommandRequest implements Serializable, Resetable
&& Objects.equal(pagingStart, other.pagingStart)
&& Objects.equal(pagingLimit, other.pagingLimit)
&& Objects.equal(path, other.path)
&& Objects.equal(branch, other.branch);
&& Objects.equal(branch, other.branch)
&& Objects.equal(ancestorChangeset, other.ancestorChangeset);
//J+
}
@@ -98,7 +99,7 @@ public final class LogCommandRequest implements Serializable, Resetable
public int hashCode()
{
return Objects.hashCode(startChangeset, endChangeset, pagingStart,
pagingLimit, path, branch);
pagingLimit, path, branch, ancestorChangeset);
}
/**
@@ -114,6 +115,7 @@ public final class LogCommandRequest implements Serializable, Resetable
pagingLimit = 20;
path = null;
branch = null;
ancestorChangeset = null;
}
/**
@@ -133,6 +135,7 @@ public final class LogCommandRequest implements Serializable, Resetable
.add("pagingLimit", pagingLimit)
.add("path", path)
.add("branch", branch)
.add("ancestorChangeset", ancestorChangeset)
.toString();
//J+
}
@@ -205,6 +208,10 @@ public final class LogCommandRequest implements Serializable, Resetable
this.startChangeset = startChangeset;
}
public void setAncestorChangeset(String ancestorChangeset) {
this.ancestorChangeset = ancestorChangeset;
}
//~--- get methods ----------------------------------------------------------
/**
@@ -284,6 +291,10 @@ public final class LogCommandRequest implements Serializable, Resetable
return pagingLimit < 0;
}
public String getAncestorChangeset() {
return ancestorChangeset;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@@ -303,4 +314,6 @@ public final class LogCommandRequest implements Serializable, Resetable
/** Field description */
private String startChangeset;
private String ancestorChangeset;
}

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;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.RevisionNotFoundException;
import java.io.IOException;
@@ -46,8 +45,8 @@ import java.io.IOException;
*/
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);
}
/**
* @since 2.0
*/
public MergeCommand getMergeCommand()
{
throw new CommandNotSupportedException(Command.MERGE);
}
}

View File

@@ -1,11 +1,19 @@
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 ChangePasswordNotAllowedException(String type) {
super(String.format(WRONG_USER_TYPE, type));
public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String 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;
public class InvalidPasswordException extends RuntimeException {
import sonia.scm.ContextEntry;
import sonia.scm.ExceptionWithContext;
public InvalidPasswordException() {
super("The given Password does not match with the stored one.");
public class InvalidPasswordException extends ExceptionWithContext {
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 SOURCE = PREFIX + "source" + SUFFIX;
public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX;
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);
}

View File

@@ -38,37 +38,17 @@ package sonia.scm.web.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
*
* @author Sebastian Sdorra
*/
public class SecurityHttpServletRequestWrapper extends HttpServletRequestWrapper
{
public class PropagatePrincipleServletRequestWrapper extends HttpServletRequestWrapper {
/**
* Constructs ...
*
*
* @param request
* @param principal
*/
public SecurityHttpServletRequestWrapper(HttpServletRequest request,
String principal)
{
private final String principal;
public PropagatePrincipleServletRequestWrapper(HttpServletRequest request, String principal) {
super(request);
this.principal = principal;
}
//~--- get methods ----------------------------------------------------------
@Override
public String getRemoteUser()
{
public String getRemoteUser() {
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.junit.MockitoJUnitRunner;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupNames;
@@ -132,7 +131,7 @@ public class SyncingRealmHelperTest {
* @throws IOException
*/
@Test
public void testStoreGroupCreate() throws AlreadyExistsException {
public void testStoreGroupCreate() {
Group group = new Group("unit-test", "heartOfGold");
helper.store(group);
@@ -143,7 +142,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(Group)}.
*/
@Test(expected = IllegalStateException.class)
public void testStoreGroupFailure() throws AlreadyExistsException {
public void testStoreGroupFailure() {
Group group = new Group("unit-test", "heartOfGold");
doThrow(AlreadyExistsException.class).when(groupManager).create(group);
@@ -169,7 +168,7 @@ public class SyncingRealmHelperTest {
* @throws IOException
*/
@Test
public void testStoreUserCreate() throws AlreadyExistsException {
public void testStoreUserCreate() {
User user = new User("tricia");
helper.store(user);
@@ -180,7 +179,7 @@ public class SyncingRealmHelperTest {
* Tests {@link SyncingRealmHelper#store(User)} with a thrown {@link AlreadyExistsException}.
*/
@Test(expected = IllegalStateException.class)
public void testStoreUserFailure() throws AlreadyExistsException {
public void testStoreUserFailure() {
User user = new User("tricia");
doThrow(AlreadyExistsException.class).when(userManager).create(user);

View File

@@ -80,13 +80,6 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>

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

@@ -0,0 +1,265 @@
package sonia.scm.it;
import org.apache.http.HttpStatus;
import org.assertj.core.util.Lists;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.client.api.RepositoryClient;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
public class DiffITCase {
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private RepositoryClient svnRepositoryClient;
private RepositoryClient gitRepositoryClient;
private RepositoryClient hgRepositoryClient;
private ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> svnRepositoryResponse;
private ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> hgRepositoryResponse;
private ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> gitRepositoryResponse;
private File svnFolder;
private File gitFolder;
private File hgFolder;
@Before
public void init() throws IOException {
TestData.createDefault();
String namespace = ADMIN_USERNAME;
String repo = TestData.getDefaultRepoName("svn");
svnRepositoryResponse =
ScmRequests.start()
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK);
svnFolder = tempFolder.newFolder("svn");
svnRepositoryClient = RepositoryUtil.createRepositoryClient("svn", svnFolder);
repo = TestData.getDefaultRepoName("git");
gitRepositoryResponse =
ScmRequests.start()
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK);
gitFolder = tempFolder.newFolder("git");
gitRepositoryClient = RepositoryUtil.createRepositoryClient("git", gitFolder);
repo = TestData.getDefaultRepoName("hg");
hgRepositoryResponse =
ScmRequests.start()
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK);
hgFolder = tempFolder.newFolder("hg");
hgRepositoryClient = RepositoryUtil.createRepositoryClient("hg", hgFolder);
}
@Test
public void shouldFindDiffsInGitFormat() throws IOException {
String svnDiff = getDiff(RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
String hgDiff = getDiff(RepositoryUtil.createAndCommitFile(hgRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), hgRepositoryResponse);
assertThat(Lists.newArrayList(svnDiff, gitDiff, hgDiff))
.allSatisfy(diff -> assertThat(diff)
.contains("diff --git "));
}
@Test
public void svnAddFileDiffShouldBeConvertedToGitDiff() throws IOException {
String svnDiff = getDiff(RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
}
@Test
public void svnDeleteFileDiffShouldBeConvertedToGitDiff() throws IOException {
RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
String svnDiff = getDiff(RepositoryUtil.removeAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt"), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
}
@Test
public void svnUpdateFileDiffShouldBeConvertedToGitDiff() throws IOException {
RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
}
@Test
public void svnMultipleChangesDiffShouldBeConvertedToGitDiff() throws IOException {
String svnDiff = getDiff(applyMultipleChanges(svnRepositoryClient, "fileToBeDeleted.txt", "fileToBeUpdated.txt", "addedFile.txt"), svnRepositoryResponse);
String gitDiff = getDiff(applyMultipleChanges(gitRepositoryClient, "fileToBeDeleted.txt", "fileToBeUpdated.txt", "addedFile.txt"), gitRepositoryResponse);
String endOfDiffPart = "\\ No newline at end of file\n";
String[] gitDiffs = gitDiff.split(endOfDiffPart);
List<String> expected = Arrays.stream(gitDiffs)
.map(this::getGitDiffWithoutIndexLine)
.collect(Collectors.toList());
assertThat(svnDiff.split(endOfDiffPart))
.containsExactlyInAnyOrderElementsOf(expected);
}
@Test
public void svnMultipleSubFolderChangesDiffShouldBeConvertedToGitDiff() throws IOException {
String svnDiff = getDiff(applyMultipleChanges(svnRepositoryClient, "a/b/fileToBeDeleted.txt", "a/c/fileToBeUpdated.txt", "a/d/addedFile.txt"), svnRepositoryResponse);
String gitDiff = getDiff(applyMultipleChanges(gitRepositoryClient, "a/b/fileToBeDeleted.txt", "a/c/fileToBeUpdated.txt", "a/d/addedFile.txt"), gitRepositoryResponse);
String endOfDiffPart = "\\ No newline at end of file\n";
String[] gitDiffs = gitDiff.split(endOfDiffPart);
List<String> expected = Arrays.stream(gitDiffs)
.map(this::getGitDiffWithoutIndexLine)
.collect(Collectors.toList());
assertThat(svnDiff.split(endOfDiffPart))
.containsExactlyInAnyOrderElementsOf(expected);
}
@Test
public void svnLargeChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
String fileName = "SvnDiffGenerator_forTest";
RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, "");
RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, "");
String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest");
String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest");
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest");
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
}
/**
* FIXME: the binary Git Diff output is not GIT conform
*/
@Test
@Ignore
@SuppressWarnings("squid:S1607")
public void svnBinaryChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
String fileName = "binary";
File file = new File(svnRepositoryClient.getWorkingCopy(), fileName);
Files.copy(Paths.get(getClass().getResource("/diff/binaryfile/echo").toURI()), Paths.get(file.toURI()));
Changeset commit = RepositoryUtil.addFileAndCommit(svnRepositoryClient, fileName, ADMIN_USERNAME, "");
file = new File(gitRepositoryClient.getWorkingCopy(), fileName);
Files.copy(Paths.get(getClass().getResource("/diff/binaryfile/echo").toURI()), Paths.get(file.toURI()));
Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, "");
String svnDiff = getDiff(commit, svnRepositoryResponse);
String gitDiff = getDiff(commit1, gitRepositoryResponse);
assertThat(svnDiff)
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
}
@Test
public void svnRenameChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
String fileName = "a.txt";
RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, "content of a");
RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, "content of a");
String newFileName = "renamed_a.txt";
File file = new File(svnRepositoryClient.getWorkingCopy(), fileName);
file.renameTo(new File(svnRepositoryClient.getWorkingCopy(), newFileName));
String svnDiff = getDiff(RepositoryUtil.addFileAndCommit(svnRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), svnRepositoryResponse);
file = new File(gitRepositoryClient.getWorkingCopy(), fileName);
file.renameTo(new File(gitRepositoryClient.getWorkingCopy(), newFileName));
String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff)
.isEqualTo(expected);
}
public String getFileContent(String name) throws URISyntaxException, IOException {
Path path;
path = Paths.get(getClass().getResource(name).toURI());
Stream<String> lines = Files.lines(path);
String data = lines.collect(Collectors.joining("\n"));
lines.close();
return data;
}
/**
* The index line is not provided from the svn git formatter and it is not needed in the ui diff view
* for more details about the git diff format: https://git-scm.com/docs/git-diff
*
* @param gitDiff
* @return diff without the index line
*/
private String getGitDiffWithoutIndexLine(String gitDiff) {
return gitDiff.replaceAll(".*(index.*\n)", "");
}
private String getDiff(Changeset svnChangeset, ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> svnRepositoryResponse) {
return svnRepositoryResponse.requestChangesets()
.requestDiffInGitFormat(svnChangeset.getId())
.getResponse()
.body()
.asString();
}
private Changeset applyMultipleChanges(RepositoryClient repositoryClient, String fileToBeDeleted, final String fileToBeUpdated, final String addedFile) throws IOException {
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileToBeDeleted, "file to be deleted");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileToBeUpdated, "file to be updated");
Map<String, String> addedFiles = new HashMap<String, String>() {{
put(addedFile, "content");
}};
Map<String, String> modifiedFiles = new HashMap<String, String>() {{
put(fileToBeUpdated, "the updated content");
}};
ArrayList<String> removedFiles = Lists.newArrayList(fileToBeDeleted);
return RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
}
}

View File

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

View File

@@ -1,5 +1,6 @@
package sonia.scm.it;
import groovy.util.logging.Slf4j;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.apache.http.HttpStatus;
@@ -37,6 +38,7 @@ import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class)
@Slf4j
public class RepositoryAccessITCase {
@Rule
@@ -63,9 +65,9 @@ public class RepositoryAccessITCase {
String repo = TestData.getDefaultRepoName(repositoryType);
repositoryResponse =
ScmRequests.start()
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK);
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK);
}
@Test
@@ -175,6 +177,7 @@ public class RepositoryAccessITCase {
}
@Test
@SuppressWarnings("squid:S2925")
public void shouldReadContent() throws IOException, InterruptedException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a");
@@ -262,40 +265,6 @@ public class RepositoryAccessITCase {
assertThat(changesets).size().isBetween(2, 3); // svn has an implicit root revision '0' that is extra to the two commits
}
@Test
public void shouldFindDiffs() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a");
RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "b.txt", "b");
String changesetsUrl = given()
.when()
.get(TestData.getDefaultRepositoryUrl(repositoryType))
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.path("_links.changesets.href");
String diffUrl = given()
.when()
.get(changesetsUrl)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.path("_embedded.changesets[0]._links.diff.href");
given()
.when()
.get(diffUrl)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.body()
.asString()
.contains("diff");
}
@Test
@SuppressWarnings("unchecked")
@@ -393,12 +362,10 @@ public class RepositoryAccessITCase {
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "b.txt", "b");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "c.txt", "c");
RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "d.txt", "d");
Map<String, String> addedFiles = new HashMap<String, String>()
{{
Map<String, String> addedFiles = new HashMap<String, String>() {{
put("a.txt", "bla bla");
}};
Map<String, String> modifiedFiles = new HashMap<String, String>()
{{
Map<String, String> modifiedFiles = new HashMap<String, String>() {{
put("b.txt", "new content");
}};
ArrayList<String> removedFiles = Lists.newArrayList("c.txt", "d.txt");
@@ -414,7 +381,7 @@ public class RepositoryAccessITCase {
.assertAdded(a -> assertThat(a)
.hasSize(1)
.containsExactly("a.txt"))
.assertModified(m-> assertThat(m)
.assertModified(m -> assertThat(m)
.hasSize(1)
.containsExactly("b.txt"))
.assertRemoved(r -> assertThat(r)

View File

@@ -80,6 +80,11 @@ public class RepositoryUtil {
return file;
}
public static Changeset updateAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
writeAndAddFile(repositoryClient, fileName, content);
return commit(repositoryClient, username, "updated " + fileName);
}
public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
return commit(repositoryClient, username, "removed " + fileName);
@@ -102,11 +107,21 @@ public class RepositoryUtil {
} else {
path = thisName;
}
repositoryClient.getAddCommand().add(path);
addFile(repositoryClient, path);
return path;
}
static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException {
public static Changeset addFileAndCommit(RepositoryClient repositoryClient, String path, String username, String message) throws IOException {
repositoryClient.getAddCommand().add(path);
return commit(repositoryClient, username, message);
}
public static void addFile(RepositoryClient repositoryClient, String path) throws IOException {
repositoryClient.getAddCommand().add(path);
}
public static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException {
LOG.info("user: {} try to commit with message: {}", username, message);
Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {

View File

@@ -38,6 +38,10 @@ public class ScmRequests {
return new ScmRequests();
}
public IndexResponse requestIndexResource() {
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
}
public IndexResponse requestIndexResource(String username, String password) {
setUsername(username);
setPassword(password);
@@ -234,8 +238,8 @@ public class ScmRequests {
return this;
}
public DiffResponse<ChangesetsResponse> requestDiff(String revision) {
return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this);
public DiffResponse<ChangesetsResponse> requestDiffInGitFormat(String revision) {
return new DiffResponse<>(applyGETRequestFromLinkWithParams(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href", "?format=GIT"), this);
}
public ModificationsResponse<ChangesetsResponse> requestModifications(String revision) {
@@ -362,6 +366,10 @@ public class ScmRequests {
this.previousResponse = previousResponse;
}
public Response getResponse(){
return response;
}
public PREV returnToPrevious() {
return previousResponse;
}

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)
.extract()
.body().jsonPath().getList("_embedded.permissions");
.body().jsonPath().<Map>getList("_embedded.permissions");
}
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) {

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -113,92 +113,29 @@
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-2</version>
<version>1.0.0-alpha-3</version>
<extensions>true</extensions>
<configuration>
<disableCompression>true</disableCompression>
</configuration>
<executions>
<execution>
<id>fix-descriptor</id>
<phase>process-resources</phase>
<goals>
<goal>fix-descriptor</goal>
</goals>
</execution>
<execution>
<id>append-dependencies</id>
<phase>process-classes</phase>
<goals>
<goal>append-dependencies</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<configuration>
<node>
<version>${nodejs.version}</version>
</node>
<pkgManager>
<type>YARN</type>
<version>${yarn.version}</version>
</pkgManager>
<failOnMissingPackageJson>false</failOnMissingPackageJson>
<script>build</script>
</configuration>
<executions>
<execution>
<id>install</id>
<phase>process-resources</phase>
<goals>
<goal>install</goal>
</goals>
</execution>
<execution>
<id>build</id>
<phase>compile</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<configuration>
<node>
<version>${nodejs.version}</version>
</node>
<pkgManager>
<type>YARN</type>
<version>${yarn.version}</version>
</pkgManager>
<failOnMissingPackageJson>false</failOnMissingPackageJson>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>sonia.maven</groupId>
<artifactId>web-compressor</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compress-directory</goal>
</goals>
</execution>
</executions>
<configuration>
<replace>true</replace>
<baseDirectory>${project.build.directory}/classes</baseDirectory>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>doc</id>

View File

@@ -4,5 +4,6 @@
[include]
[libs]
./node_modules/@scm-manager/ui-components/flow-typed
[options]

View File

@@ -1,23 +0,0 @@
// flow-typed signature: cf86673cc32d185bdab1d2ea90578d37
// flow-typed version: 614bf49aa8/classnames_v2.x.x/flow_>=v0.25.x
type $npm$classnames$Classes =
| string
| { [className: string]: * }
| false
| void
| null;
declare module "classnames" {
declare module.exports: (
...classes: Array<$npm$classnames$Classes | $npm$classnames$Classes[]>
) => string;
}
declare module "classnames/bind" {
declare module.exports: $Exports<"classnames">;
}
declare module "classnames/dedupe" {
declare module.exports: $Exports<"classnames">;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,331 +0,0 @@
// flow-typed signature: 23b805356f90ad9384dd88489654e380
// flow-typed version: e9374c5fe9/moment_v2.3.x/flow_>=v0.25.x
type moment$MomentOptions = {
y?: number | string,
year?: number | string,
years?: number | string,
M?: number | string,
month?: number | string,
months?: number | string,
d?: number | string,
day?: number | string,
days?: number | string,
date?: number | string,
h?: number | string,
hour?: number | string,
hours?: number | string,
m?: number | string,
minute?: number | string,
minutes?: number | string,
s?: number | string,
second?: number | string,
seconds?: number | string,
ms?: number | string,
millisecond?: number | string,
milliseconds?: number | string
};
type moment$MomentObject = {
years: number,
months: number,
date: number,
hours: number,
minutes: number,
seconds: number,
milliseconds: number
};
type moment$MomentCreationData = {
input: string,
format: string,
locale: Object,
isUTC: boolean,
strict: boolean
};
type moment$CalendarFormat = string | ((moment: moment$Moment) => string);
type moment$CalendarFormats = {
sameDay?: moment$CalendarFormat,
nextDay?: moment$CalendarFormat,
nextWeek?: moment$CalendarFormat,
lastDay?: moment$CalendarFormat,
lastWeek?: moment$CalendarFormat,
sameElse?: moment$CalendarFormat
};
declare class moment$LocaleData {
months(moment: moment$Moment): string,
monthsShort(moment: moment$Moment): string,
monthsParse(month: string): number,
weekdays(moment: moment$Moment): string,
weekdaysShort(moment: moment$Moment): string,
weekdaysMin(moment: moment$Moment): string,
weekdaysParse(weekDay: string): number,
longDateFormat(dateFormat: string): string,
isPM(date: string): boolean,
meridiem(hours: number, minutes: number, isLower: boolean): string,
calendar(
key:
| "sameDay"
| "nextDay"
| "lastDay"
| "nextWeek"
| "prevWeek"
| "sameElse",
moment: moment$Moment
): string,
relativeTime(
number: number,
withoutSuffix: boolean,
key: "s" | "m" | "mm" | "h" | "hh" | "d" | "dd" | "M" | "MM" | "y" | "yy",
isFuture: boolean
): string,
pastFuture(diff: any, relTime: string): string,
ordinal(number: number): string,
preparse(str: string): any,
postformat(str: string): any,
week(moment: moment$Moment): string,
invalidDate(): string,
firstDayOfWeek(): number,
firstDayOfYear(): number
}
declare class moment$MomentDuration {
humanize(suffix?: boolean): string,
milliseconds(): number,
asMilliseconds(): number,
seconds(): number,
asSeconds(): number,
minutes(): number,
asMinutes(): number,
hours(): number,
asHours(): number,
days(): number,
asDays(): number,
months(): number,
asWeeks(): number,
weeks(): number,
asMonths(): number,
years(): number,
asYears(): number,
add(value: number | moment$MomentDuration | Object, unit?: string): this,
subtract(value: number | moment$MomentDuration | Object, unit?: string): this,
as(unit: string): number,
get(unit: string): number,
toJSON(): string,
toISOString(): string,
isValid(): boolean
}
declare class moment$Moment {
static ISO_8601: string,
static (
string?: string,
format?: string | Array<string>,
strict?: boolean
): moment$Moment,
static (
string?: string,
format?: string | Array<string>,
locale?: string,
strict?: boolean
): moment$Moment,
static (
initDate: ?Object | number | Date | Array<number> | moment$Moment | string
): moment$Moment,
static unix(seconds: number): moment$Moment,
static utc(): moment$Moment,
static utc(number: number | Array<number>): moment$Moment,
static utc(
str: string,
str2?: string | Array<string>,
str3?: string
): moment$Moment,
static utc(moment: moment$Moment): moment$Moment,
static utc(date: Date): moment$Moment,
static parseZone(): moment$Moment,
static parseZone(rawDate: string): moment$Moment,
static parseZone(
rawDate: string,
format: string | Array<string>
): moment$Moment,
static parseZone(
rawDate: string,
format: string,
strict: boolean
): moment$Moment,
static parseZone(
rawDate: string,
format: string,
locale: string,
strict: boolean
): moment$Moment,
isValid(): boolean,
invalidAt(): 0 | 1 | 2 | 3 | 4 | 5 | 6,
creationData(): moment$MomentCreationData,
millisecond(number: number): this,
milliseconds(number: number): this,
millisecond(): number,
milliseconds(): number,
second(number: number): this,
seconds(number: number): this,
second(): number,
seconds(): number,
minute(number: number): this,
minutes(number: number): this,
minute(): number,
minutes(): number,
hour(number: number): this,
hours(number: number): this,
hour(): number,
hours(): number,
date(number: number): this,
dates(number: number): this,
date(): number,
dates(): number,
day(day: number | string): this,
days(day: number | string): this,
day(): number,
days(): number,
weekday(number: number): this,
weekday(): number,
isoWeekday(number: number): this,
isoWeekday(): number,
dayOfYear(number: number): this,
dayOfYear(): number,
week(number: number): this,
weeks(number: number): this,
week(): number,
weeks(): number,
isoWeek(number: number): this,
isoWeeks(number: number): this,
isoWeek(): number,
isoWeeks(): number,
month(number: number): this,
months(number: number): this,
month(): number,
months(): number,
quarter(number: number): this,
quarter(): number,
year(number: number): this,
years(number: number): this,
year(): number,
years(): number,
weekYear(number: number): this,
weekYear(): number,
isoWeekYear(number: number): this,
isoWeekYear(): number,
weeksInYear(): number,
isoWeeksInYear(): number,
get(string: string): number,
set(unit: string, value: number): this,
set(options: { [unit: string]: number }): this,
static max(...dates: Array<moment$Moment>): moment$Moment,
static max(dates: Array<moment$Moment>): moment$Moment,
static min(...dates: Array<moment$Moment>): moment$Moment,
static min(dates: Array<moment$Moment>): moment$Moment,
add(
value: number | moment$MomentDuration | moment$Moment | Object,
unit?: string
): this,
subtract(
value: number | moment$MomentDuration | moment$Moment | string | Object,
unit?: string
): this,
startOf(unit: string): this,
endOf(unit: string): this,
local(): this,
utc(): this,
utcOffset(
offset: number | string,
keepLocalTime?: boolean,
keepMinutes?: boolean
): this,
utcOffset(): number,
format(format?: string): string,
fromNow(removeSuffix?: boolean): string,
from(
value: moment$Moment | string | number | Date | Array<number>,
removePrefix?: boolean
): string,
toNow(removePrefix?: boolean): string,
to(
value: moment$Moment | string | number | Date | Array<number>,
removePrefix?: boolean
): string,
calendar(refTime?: any, formats?: moment$CalendarFormats): string,
diff(
date: moment$Moment | string | number | Date | Array<number>,
format?: string,
floating?: boolean
): number,
valueOf(): number,
unix(): number,
daysInMonth(): number,
toDate(): Date,
toArray(): Array<number>,
toJSON(): string,
toISOString(
keepOffset?: boolean
): string,
toObject(): moment$MomentObject,
isBefore(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isSame(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isAfter(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isSameOrBefore(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isSameOrAfter(
date?: moment$Moment | string | number | Date | Array<number>,
units?: ?string
): boolean,
isBetween(
fromDate: moment$Moment | string | number | Date | Array<number>,
toDate?: ?moment$Moment | string | number | Date | Array<number>,
granularity?: ?string,
inclusion?: ?string
): boolean,
isDST(): boolean,
isDSTShifted(): boolean,
isLeapYear(): boolean,
clone(): moment$Moment,
static isMoment(obj: any): boolean,
static isDate(obj: any): boolean,
static locale(locale: string, localeData?: Object): string,
static updateLocale(locale: string, localeData?: ?Object): void,
static locale(locales: Array<string>): string,
locale(locale: string, customization?: Object | null): moment$Moment,
locale(): string,
static months(): Array<string>,
static monthsShort(): Array<string>,
static weekdays(): Array<string>,
static weekdaysShort(): Array<string>,
static weekdaysMin(): Array<string>,
static months(): string,
static monthsShort(): string,
static weekdays(): string,
static weekdaysShort(): string,
static weekdaysMin(): string,
static localeData(key?: string): moment$LocaleData,
static duration(
value: number | Object | string,
unit?: string
): moment$MomentDuration,
static isDuration(obj: any): boolean,
static normalizeUnits(unit: string): string,
static invalid(object: any): moment$Moment
}
declare module "moment" {
declare module.exports: Class<moment$Moment>;
}

View File

@@ -1,137 +0,0 @@
// flow-typed signature: ba35d02d668b0d0a3e04a63a6847974e
// flow-typed version: <<STUB>>/react-jss_v8.6.1/flow_v0.79.1
/**
* This is an autogenerated libdef stub for:
*
* 'react-jss'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-jss' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'react-jss/dist/react-jss' {
declare module.exports: any;
}
declare module 'react-jss/dist/react-jss.min' {
declare module.exports: any;
}
declare module 'react-jss/lib/compose' {
declare module.exports: any;
}
declare module 'react-jss/lib/compose.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/contextTypes' {
declare module.exports: any;
}
declare module 'react-jss/lib/createHoc' {
declare module.exports: any;
}
declare module 'react-jss/lib/getDisplayName' {
declare module.exports: any;
}
declare module 'react-jss/lib/index' {
declare module.exports: any;
}
declare module 'react-jss/lib/index.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/injectSheet' {
declare module.exports: any;
}
declare module 'react-jss/lib/injectSheet.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/jss' {
declare module.exports: any;
}
declare module 'react-jss/lib/JssProvider' {
declare module.exports: any;
}
declare module 'react-jss/lib/JssProvider.test' {
declare module.exports: any;
}
declare module 'react-jss/lib/ns' {
declare module.exports: any;
}
declare module 'react-jss/lib/propTypes' {
declare module.exports: any;
}
// Filename aliases
declare module 'react-jss/dist/react-jss.js' {
declare module.exports: $Exports<'react-jss/dist/react-jss'>;
}
declare module 'react-jss/dist/react-jss.min.js' {
declare module.exports: $Exports<'react-jss/dist/react-jss.min'>;
}
declare module 'react-jss/lib/compose.js' {
declare module.exports: $Exports<'react-jss/lib/compose'>;
}
declare module 'react-jss/lib/compose.test.js' {
declare module.exports: $Exports<'react-jss/lib/compose.test'>;
}
declare module 'react-jss/lib/contextTypes.js' {
declare module.exports: $Exports<'react-jss/lib/contextTypes'>;
}
declare module 'react-jss/lib/createHoc.js' {
declare module.exports: $Exports<'react-jss/lib/createHoc'>;
}
declare module 'react-jss/lib/getDisplayName.js' {
declare module.exports: $Exports<'react-jss/lib/getDisplayName'>;
}
declare module 'react-jss/lib/index.js' {
declare module.exports: $Exports<'react-jss/lib/index'>;
}
declare module 'react-jss/lib/index.test.js' {
declare module.exports: $Exports<'react-jss/lib/index.test'>;
}
declare module 'react-jss/lib/injectSheet.js' {
declare module.exports: $Exports<'react-jss/lib/injectSheet'>;
}
declare module 'react-jss/lib/injectSheet.test.js' {
declare module.exports: $Exports<'react-jss/lib/injectSheet.test'>;
}
declare module 'react-jss/lib/jss.js' {
declare module.exports: $Exports<'react-jss/lib/jss'>;
}
declare module 'react-jss/lib/JssProvider.js' {
declare module.exports: $Exports<'react-jss/lib/JssProvider'>;
}
declare module 'react-jss/lib/JssProvider.test.js' {
declare module.exports: $Exports<'react-jss/lib/JssProvider.test'>;
}
declare module 'react-jss/lib/ns.js' {
declare module.exports: $Exports<'react-jss/lib/ns'>;
}
declare module 'react-jss/lib/propTypes.js' {
declare module.exports: $Exports<'react-jss/lib/propTypes'>;
}

View File

@@ -1,14 +1,17 @@
{
"name": "@scm-manager/scm-git-plugin",
"license" : "BSD-3-Clause",
"license": "BSD-3-Clause",
"main": "src/main/js/index.js",
"scripts": {
"build": "ui-bundler plugin"
"build": "ui-bundler plugin",
"watch": "ui-bundler plugin -w",
"lint": "ui-bundler lint",
"flow": "flow check"
},
"dependencies": {
"@scm-manager/ui-extensions": "^0.0.7"
"@scm-manager/ui-extensions": "^0.1.1"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.17"
"@scm-manager/ui-bundler": "^0.0.21"
}
}

View File

@@ -43,11 +43,24 @@
</dependencies>
<!-- create test jar -->
<build>
<plugins>
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<corePlugin>true</corePlugin>
<links>
<link>@scm-manager/ui-types</link>
<link>@scm-manager/ui-components</link>
</links>
</configuration>
</plugin>
<!-- create test jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@@ -61,33 +74,6 @@
</executions>
</plugin>
<plugin>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<executions>
<execution>
<id>link-ui-types</id>
<phase>process-sources</phase>
<goals>
<goal>install-link</goal>
</goals>
<configuration>
<pkg>@scm-manager/ui-types</pkg>
</configuration>
</execution>
<execution>
<id>link-ui-components</id>
<phase>process-sources</phase>
<goals>
<goal>install-link</goal>
</goals>
<configuration>
<pkg>@scm-manager/ui-components</pkg>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

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 GitWorkdirFactory workdirFactory;
private Task task;
//~--- constructors ---------------------------------------------------------
@@ -98,16 +100,18 @@ public class GitRepositoryHandler
/**
* Constructs ...
*
* @param storeFactory
*
* @param storeFactory
* @param fileSystem
* @param scheduler
* @param repositoryLocationResolver
*/
@Inject
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver)
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, RepositoryLocationResolver repositoryLocationResolver, GitWorkdirFactory workdirFactory)
{
super(storeFactory, fileSystem, repositoryLocationResolver);
this.scheduler = scheduler;
this.workdirFactory = workdirFactory;
}
//~--- get methods ----------------------------------------------------------
@@ -116,17 +120,17 @@ public class GitRepositoryHandler
public void init(SCMContextProvider context)
{
super.init(context);
scheduleGc();
scheduleGc(getConfig().getGcExpression());
}
@Override
public void setConfig(GitConfig config)
{
scheduleGc(config.getGcExpression());
super.setConfig(config);
scheduleGc();
}
private void scheduleGc()
private void scheduleGc(String expression)
{
synchronized (LOCK){
if ( task != null ){
@@ -134,11 +138,10 @@ public class GitRepositoryHandler
task.cancel();
task = null;
}
String exp = getConfig().getGcExpression();
if (!Strings.isNullOrEmpty(exp))
if (!Strings.isNullOrEmpty(expression))
{
logger.info("schedule git gc task with expression {}", exp);
task = scheduler.schedule(exp, GitGcTask.class);
logger.info("schedule git gc task with expression {}", expression);
task = scheduler.schedule(expression, GitGcTask.class);
}
}
}
@@ -235,4 +238,8 @@ public class GitRepositoryHandler
{
return new File(directory, DIRECTORY_REFS).exists();
}
public GitWorkdirFactory getWorkdirFactory() {
return workdirFactory;
}
}

View File

@@ -48,6 +48,7 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
@@ -55,6 +56,7 @@ import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import sonia.scm.web.GitUserAgentProvider;
@@ -203,7 +205,7 @@ public final class GitUtil
}
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);
}
}
@@ -716,6 +718,18 @@ public final class GitUtil
return (id != null) &&!id.equals(ObjectId.zeroId());
}
/**
* Computes the first common ancestor of two revisions, aka merge base.
*/
public static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1));
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2));
return mergeBaseWalk.next().getId();
}
}
//~--- methods --------------------------------------------------------------
/**

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

View File

@@ -114,7 +114,7 @@ public abstract class AbstractGitPushOrPullCommand extends AbstractGitCommand
}
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;

View File

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

View File

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

View File

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

View File

@@ -45,8 +45,6 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.Util;
import java.io.Closeable;
@@ -55,6 +53,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class GitCatCommand extends AbstractGitCommand implements CatCommand {
@@ -65,7 +66,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
}
@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);
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) {
closableObjectLoaderContainer.objectLoader.copyTo(output);
@@ -73,24 +74,24 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
}
@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);
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)) {
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();
ObjectId revId = getCommitOrDefault(repo, request.getRevision());
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.setRecursive(Util.nonNull(path).contains("/"));
@@ -102,7 +103,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
try {
entry = revWalk.parseCommit(revId);
} catch (MissingObjectException e) {
throw new RevisionNotFoundException(revId.getName());
throw notFound(entity("Revision", revId.getName()).in(repository));
}
RevTree revTree = entry.getTree();
@@ -120,7 +121,7 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk);
} 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 sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
//~--- JDK imports ------------------------------------------------------------
@@ -65,10 +66,12 @@ public class GitContext implements Closeable
*
*
* @param directory
* @param repository
*/
public GitContext(File directory)
public GitContext(File directory, Repository repository)
{
this.directory = directory;
this.repository = repository;
}
//~--- methods --------------------------------------------------------------
@@ -82,8 +85,8 @@ public class GitContext implements Closeable
{
logger.trace("close git repository {}", directory);
GitUtil.close(repository);
repository = null;
GitUtil.close(gitRepository);
gitRepository = null;
}
/**
@@ -96,21 +99,30 @@ public class GitContext implements Closeable
*/
public org.eclipse.jgit.lib.Repository open() throws IOException
{
if (repository == null)
if (gitRepository == null)
{
logger.trace("open git repository {}", directory);
repository = GitUtil.open(directory);
gitRepository = GitUtil.open(directory);
}
return gitRepository;
}
Repository getRepository() {
return repository;
}
File getDirectory() {
return directory;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final File directory;
private final Repository repository;
/** Field description */
private org.eclipse.jgit.lib.Repository repository;
private org.eclipse.jgit.lib.Repository gitRepository;
}

View File

@@ -34,27 +34,25 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
/**
@@ -107,7 +105,8 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
walk = new RevWalk(gr);
RevCommit commit = walk.parseCommit(gr.resolve(request.getRevision()));
ObjectId revision = gr.resolve(request.getRevision());
RevCommit commit = walk.parseCommit(revision);
walk.markStart(commit);
commit = walk.next();
@@ -120,7 +119,15 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
treeWalk.setFilter(PathFilter.create(request.getPath()));
}
if (commit.getParentCount() > 0)
if (!Strings.isNullOrEmpty(request.getAncestorChangeset()))
{
ObjectId otherRevision = gr.resolve(request.getAncestorChangeset());
ObjectId ancestorId = computeCommonAncestor(gr, revision, otherRevision);
RevTree tree = walk.parseCommit(ancestorId).getTree();
treeWalk.addTree(tree);
}
else if (commit.getParentCount() > 0)
{
RevTree tree = commit.getParent(0).getTree();
@@ -156,7 +163,6 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
}
catch (Exception ex)
{
// TODO throw exception
logger.error("could not create diff", ex);
}
@@ -167,4 +173,9 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand
GitUtil.release(formatter);
}
}
private ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
return GitUtil.computeCommonAncestor(repository, revision1, revision2);
}
}

View File

@@ -43,6 +43,7 @@ import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
@@ -53,7 +54,6 @@ import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.IOUtil;
import java.io.IOException;
@@ -61,6 +61,9 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
//~--- JDK imports ------------------------------------------------------------
/**
@@ -85,7 +88,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
*
* @param context
* @param repository
* @param repositoryDirectory
*/
GitLogCommand(GitContext context, sonia.scm.repository.Repository repository)
{
@@ -162,7 +164,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
*/
@Override
@SuppressWarnings("unchecked")
public ChangesetPagingResult getChangesets(LogCommandRequest request) throws RevisionNotFoundException {
public ChangesetPagingResult getChangesets(LogCommandRequest request) {
if (logger.isDebugEnabled()) {
logger.debug("fetch changesets for request: {}", request);
}
@@ -198,6 +200,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
endId = repository.resolve(request.getEndChangeset());
}
Ref branch = getBranchOrDefault(repository,request.getBranch());
ObjectId ancestorId = null;
if (!Strings.isNullOrEmpty(request.getAncestorChangeset())) {
ancestorId = computeCommonAncestor(request, repository, startId, branch);
}
revWalk = new RevWalk(repository);
converter = new GitChangesetConverter(repository, revWalk);
@@ -208,8 +218,6 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
}
Ref branch = getBranchOrDefault(repository,request.getBranch());
if (branch != null) {
if (startId != null) {
revWalk.markStart(revWalk.lookupCommit(startId));
@@ -217,11 +225,16 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
}
Iterator<RevCommit> iterator = revWalk.iterator();
while (iterator.hasNext()) {
RevCommit commit = iterator.next();
if (commit.getId().equals(ancestorId)) {
break;
}
if ((counter >= start)
&& ((limit < 0) || (counter < start + limit))) {
changesetList.add(converter.createChangeset(commit));
@@ -229,7 +242,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
counter++;
if ((endId != null) && commit.getId().equals(endId)) {
if (commit.getId().equals(endId)) {
break;
}
}
@@ -249,11 +262,11 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
}
catch (MissingObjectException e)
{
throw new RevisionNotFoundException(e.getObjectId().name());
throw notFound(entity("Revision", e.getObjectId().getName()).in(repository));
}
catch (Exception ex)
{
throw new InternalRepositoryException("could not create change log", ex);
throw new InternalRepositoryException(repository, "could not create change log", ex);
}
finally
{
@@ -263,4 +276,17 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
return changesets;
}
private ObjectId computeCommonAncestor(LogCommandRequest request, Repository repository, ObjectId startId, Ref branch) throws IOException {
try (RevWalk mergeBaseWalk = new RevWalk(repository)) {
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
if (startId != null) {
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(startId));
} else {
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(branch.getObjectId()));
}
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(repository.resolve(request.getAncestorChangeset())));
return mergeBaseWalk.next().getId();
}
}
}

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.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Slf4j
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)
throws IOException, UnsupportedModificationTypeException {
throws IOException {
treeWalk.reset();
treeWalk.setRecursive(true);
if (commit.getParentCount() > 0) {
@@ -73,12 +75,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
}
} catch (IOException ex) {
log.error("could not open repository", ex);
throw new InternalRepositoryException(ex);
} catch (UnsupportedModificationTypeException ex) {
log.error("Unsupported modification type", ex);
throw new InternalRepositoryException(ex);
throw new InternalRepositoryException(entity(repository), "could not open repository", ex);
} finally {
GitUtil.release(revWalk);
GitUtil.close(gitRepository);
@@ -100,7 +97,7 @@ public class GitModificationsCommand extends AbstractGitCommand implements Modif
} else if (type == DiffEntry.ChangeType.DELETE) {
modifications.getRemoved().add(entry.getOldPath());
} 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
*
* @throws IOException
* @throws RepositoryException
*/
@Override
public ChangesetPagingResult getOutgoingChangesets(

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand
}
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
{

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;
import sonia.scm.ContextEntry;
import sonia.scm.repository.InternalRepositoryException;
public class UnsupportedModificationTypeException extends InternalRepositoryException {
public UnsupportedModificationTypeException(String message) {
super(message);
public UnsupportedModificationTypeException(ContextEntry.ContextBuilder entity, String 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
*
* @throws IOException
* @throws RepositoryException
*/
private BranchesModel createBranchesModel(Repository repository)
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.GitConfigToGitConfigDtoMapper;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
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(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
}
}

View File

@@ -0,0 +1,79 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Links } from "@scm-manager/ui-types";
import { InputField, Checkbox } from "@scm-manager/ui-components";
type Configuration = {
repositoryDirectory?: string,
gcExpression?: string,
disabled: boolean,
_links: Links
}
type Props = {
initialConfiguration: Configuration,
readOnly: boolean,
onConfigurationChange: (Configuration, boolean) => void,
// context props
t: (string) => string
}
type State = Configuration & {
}
class GitConfigurationForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { ...props.initialConfiguration };
}
isValid = () => {
return !!this.state.repositoryDirectory;
};
handleChange = (value: any, name: string) => {
this.setState({
[name]: value
}, () => this.props.onConfigurationChange(this.state, this.isValid()));
};
render() {
const { repositoryDirectory, gcExpression, disabled } = this.state;
const { readOnly, t } = this.props;
return (
<>
<InputField name="repositoryDirectory"
label={t("scm-git-plugin.config.directory")}
helpText={t("scm-git-plugin.config.directoryHelpText")}
value={repositoryDirectory}
onChange={this.handleChange}
disabled={readOnly}
/>
<InputField name="gcExpression"
label={t("scm-git-plugin.config.gcExpression")}
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
value={gcExpression}
onChange={this.handleChange}
disabled={readOnly}
/>
<Checkbox name="disabled"
label={t("scm-git-plugin.config.disabled")}
helpText={t("scm-git-plugin.config.disabledHelpText")}
checked={disabled}
onChange={this.handleChange}
disabled={readOnly}
/>
</>
);
}
}
export default translate("plugins")(GitConfigurationForm);

View File

@@ -0,0 +1,32 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { Title, GlobalConfiguration } from "@scm-manager/ui-components";
import GitConfigurationForm from "./GitConfigurationForm";
type Props = {
link: string,
t: (string) => string
};
class GitGlobalConfiguration extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
render() {
const { link, t } = this.props;
return (
<div>
<Title title={t("scm-git-plugin.config.title")}/>
<GlobalConfiguration link={link} render={props => <GitConfigurationForm {...props} />}/>
</div>
);
}
}
export default translate("plugins")(GitGlobalConfiguration);

View File

@@ -3,9 +3,18 @@ import { binder } from "@scm-manager/ui-extensions";
import ProtocolInformation from "./ProtocolInformation";
import GitAvatar from "./GitAvatar";
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
import GitGlobalConfiguration from "./GitGlobalConfiguration";
// repository
const gitPredicate = (props: Object) => {
return props.repository && props.repository.type === "git";
};
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
// global config
cfgBinder.bindGlobal("/git", "scm-git-plugin.config.link", "gitConfig", GitGlobalConfiguration);

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